Skip to content

Commit 6bccc36

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 5e56226 commit 6bccc36

File tree

2 files changed

+166
-69
lines changed

2 files changed

+166
-69
lines changed

git/index/base.py

Lines changed: 129 additions & 67 deletions
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")
@@ -610,8 +644,8 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
610644
raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
611645
return os.path.relpath(path, self.repo.working_tree_dir)
612646

613-
def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule']]
614-
) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
647+
def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule']],
648+
file_store: _FileStore) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
615649
""" Split the items into two lists of path strings and BaseEntries. """
616650
paths = []
617651
entries = []
@@ -621,6 +655,7 @@ def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexE
621655

622656
for item in items:
623657
if isinstance(item, (str, os.PathLike)):
658+
self._autocrlf(item, file_store)
624659
paths.append(self._to_relative_path(item))
625660
elif isinstance(item, (Blob, Submodule)):
626661
entries.append(BaseIndexEntry.from_blob(item))
@@ -630,6 +665,31 @@ def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexE
630665
raise TypeError("Invalid Type: %r" % item)
631666
# END for each item
632667
return paths, entries
668+
669+
def _autocrlf(self, file: PathLike, file_store: _FileStore) -> None:
670+
"""If the config option `autocrlf` is True, replace CRLF with LF"""
671+
672+
reader = self.repo.config_reader()
673+
674+
autocrlf = reader.get_value("core", "autocrlf", False)
675+
676+
if not autocrlf:
677+
return
678+
679+
file_store.save(file)
680+
681+
with tempfile.TemporaryFile("wb+") as tf:
682+
with open(file, "rb") as f:
683+
for line in f:
684+
line = line.replace(b"\r\n", b"\n")
685+
tf.write(line)
686+
687+
tf.seek(0)
688+
689+
with open(file, "wb") as f:
690+
for line in tf:
691+
f.write(line)
692+
633693

634694
def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
635695
"""Store file at filepath in the database and return the base index entry
@@ -801,73 +861,75 @@ def add(
801861
Objects that do not have a null sha will be added even if their paths
802862
do not exist.
803863
"""
804-
# sort the entries into strings and Entries, Blobs are converted to entries
805-
# automatically
806-
# paths can be git-added, for everything else we use git-update-index
807-
paths, entries = self._preprocess_add_items(items)
808-
entries_added: List[BaseIndexEntry] = []
809-
# This code needs a working tree, therefore we try not to run it unless required.
810-
# That way, we are OK on a bare repository as well.
811-
# If there are no paths, the rewriter has nothing to do either
812-
if paths:
813-
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
814-
815-
# HANDLE ENTRIES
816-
if entries:
817-
null_mode_entries = [e for e in entries if e.mode == 0]
818-
if null_mode_entries:
819-
raise ValueError(
820-
"At least one Entry has a null-mode - please use index.remove to remove files for clarity")
821-
# END null mode should be remove
822-
823-
# HANDLE ENTRY OBJECT CREATION
824-
# create objects if required, otherwise go with the existing shas
825-
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
826-
if null_entries_indices:
827-
@ git_working_dir
828-
def handle_null_entries(self: 'IndexFile') -> None:
829-
for ei in null_entries_indices:
830-
null_entry = entries[ei]
831-
new_entry = self._store_path(null_entry.path, fprogress)
832-
833-
# update null entry
834-
entries[ei] = BaseIndexEntry(
835-
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path))
836-
# END for each entry index
837-
# end closure
838-
handle_null_entries(self)
839-
# END null_entry handling
840-
841-
# REWRITE PATHS
842-
# If we have to rewrite the entries, do so now, after we have generated
843-
# all object sha's
844-
if path_rewriter:
845-
for i, e in enumerate(entries):
846-
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
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+
# END null mode should be remove
884+
885+
# HANDLE ENTRY OBJECT CREATION
886+
# create objects if required, otherwise go with the existing shas
887+
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
888+
if null_entries_indices:
889+
@ git_working_dir
890+
def handle_null_entries(self: 'IndexFile') -> None:
891+
for ei in null_entries_indices:
892+
null_entry = entries[ei]
893+
new_entry = self._store_path(null_entry.path, fprogress)
894+
895+
# update null entry
896+
entries[ei] = BaseIndexEntry(
897+
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path))
898+
# END for each entry index
899+
# end closure
900+
handle_null_entries(self)
901+
# END null_entry handling
902+
903+
# REWRITE PATHS
904+
# If we have to rewrite the entries, do so now, after we have generated
905+
# all object sha's
906+
if path_rewriter:
907+
for i, e in enumerate(entries):
908+
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
909+
# END for each entry
910+
# END handle path rewriting
911+
912+
# just go through the remaining entries and provide progress info
913+
for i, entry in enumerate(entries):
914+
progress_sent = i in null_entries_indices
915+
if not progress_sent:
916+
fprogress(entry.path, False, entry)
917+
fprogress(entry.path, True, entry)
918+
# END handle progress
847919
# END for each entry
848-
# END handle path rewriting
849-
850-
# just go through the remaining entries and provide progress info
851-
for i, entry in enumerate(entries):
852-
progress_sent = i in null_entries_indices
853-
if not progress_sent:
854-
fprogress(entry.path, False, entry)
855-
fprogress(entry.path, True, entry)
856-
# END handle progress
857-
# END for each entry
858-
entries_added.extend(entries)
859-
# END if there are base entries
860-
861-
# FINALIZE
862-
# add the new entries to this instance
863-
for entry in entries_added:
864-
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
865-
866-
if write:
867-
self.write(ignore_extension_data=not write_extension_data)
868-
# END handle write
920+
entries_added.extend(entries)
921+
# END if there are base entries
869922

870-
return entries_added
923+
# FINALIZE
924+
# add the new entries to this instance
925+
for entry in entries_added:
926+
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
927+
928+
if write:
929+
self.write(ignore_extension_data=not write_extension_data)
930+
# END handle write
931+
932+
return entries_added
871933

872934
def _items_to_rela_paths(
873935
self,

test/test_index.py

Lines changed: 37 additions & 2 deletions
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 (
@@ -28,6 +28,7 @@
2828
HookExecutionError,
2929
InvalidGitRepositoryError
3030
)
31+
from git.index.base import _FileStore
3132
from git.index.fun import hook_path
3233
from git.index.typ import BaseIndexEntry, IndexEntry
3334
from git.objects import Blob
@@ -956,4 +957,38 @@ def test_index_add_pathlike(self, rw_repo):
956957
file = git_dir / "file.txt"
957958
file.touch()
958959

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

0 commit comments

Comments
 (0)