Skip to content

Commit 6d2c5b5

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 6d2c5b5

File tree

2 files changed

+173
-77
lines changed

2 files changed

+173
-77
lines changed

git/index/base.py

+132-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,
@@ -89,6 +91,40 @@
8991
# ------------------------------------------------------------------------------------
9092

9193

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

94130

@@ -611,7 +647,7 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
611647
return os.path.relpath(path, self.repo.working_tree_dir)
612648

613649
def _preprocess_add_items(
614-
self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]
650+
self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]], file_store: _FileStore
615651
) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
616652
"""Split the items into two lists of path strings and BaseEntries."""
617653
paths = []
@@ -622,6 +658,7 @@ def _preprocess_add_items(
622658

623659
for item in items:
624660
if isinstance(item, (str, os.PathLike)):
661+
self._autocrlf(item, file_store)
625662
paths.append(self._to_relative_path(item))
626663
elif isinstance(item, (Blob, Submodule)):
627664
entries.append(BaseIndexEntry.from_blob(item))
@@ -632,6 +669,30 @@ def _preprocess_add_items(
632669
# END for each item
633670
return paths, entries
634671

672+
def _autocrlf(self, file: PathLike, file_store: _FileStore) -> None:
673+
"""If the config option `autocrlf` is True, replace CRLF with LF"""
674+
675+
reader = self.repo.config_reader()
676+
677+
autocrlf = reader.get_value("core", "autocrlf", False)
678+
679+
if not autocrlf:
680+
return
681+
682+
file_store.save(file)
683+
684+
with tempfile.TemporaryFile("wb+") as tf:
685+
with open(file, "rb") as f:
686+
for line in f:
687+
line = line.replace(b"\r\n", b"\n")
688+
tf.write(line)
689+
690+
tf.seek(0)
691+
692+
with open(file, "wb") as f:
693+
for line in tf:
694+
f.write(line)
695+
635696
def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
636697
"""Store file at filepath in the database and return the base index entry
637698
Needs the git_working_dir decorator active ! This must be assured in the calling code"""
@@ -802,82 +863,79 @@ def add(
802863
Objects that do not have a null sha will be added even if their paths
803864
do not exist.
804865
"""
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,
866+
867+
with _FileStore() as file_store:
868+
# sort the entries into strings and Entries, Blobs are converted to entries
869+
# automatically
870+
# paths can be git-added, for everything else we use git-update-index
871+
paths, entries = self._preprocess_add_items(items, file_store)
872+
entries_added: List[BaseIndexEntry] = []
873+
# This code needs a working tree, therefore we try not to run it unless required.
874+
# That way, we are OK on a bare repository as well.
875+
# If there are no paths, the rewriter has nothing to do either
876+
if paths:
877+
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
878+
879+
# HANDLE ENTRIES
880+
if entries:
881+
null_mode_entries = [e for e in entries if e.mode == 0]
882+
if null_mode_entries:
883+
raise ValueError(
884+
"At least one Entry has a null-mode - please use index.remove to remove files for clarity"
885+
)
886+
# END null mode should be remove
887+
888+
# HANDLE ENTRY OBJECT CREATION
889+
# create objects if required, otherwise go with the existing shas
890+
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
891+
if null_entries_indices:
892+
893+
@git_working_dir
894+
def handle_null_entries(self: "IndexFile") -> None:
895+
for ei in null_entries_indices:
896+
null_entry = entries[ei]
897+
new_entry = self._store_path(null_entry.path, fprogress)
898+
899+
# update null entry
900+
entries[ei] = BaseIndexEntry(
901+
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path)
843902
)
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)))
903+
# END for each entry index
904+
905+
# end closure
906+
handle_null_entries(self)
907+
# END null_entry handling
908+
909+
# REWRITE PATHS
910+
# If we have to rewrite the entries, do so now, after we have generated
911+
# all object sha's
912+
if path_rewriter:
913+
for i, e in enumerate(entries):
914+
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
915+
# END for each entry
916+
# END handle path rewriting
917+
918+
# just go through the remaining entries and provide progress info
919+
for i, entry in enumerate(entries):
920+
progress_sent = i in null_entries_indices
921+
if not progress_sent:
922+
fprogress(entry.path, False, entry)
923+
fprogress(entry.path, True, entry)
924+
# END handle progress
857925
# 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
926+
entries_added.extend(entries)
927+
# END if there are base entries
879928

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

882940
def _items_to_rela_paths(
883941
self,

test/test_index.py

+41-3
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,11 @@
2424
CheckoutError,
2525
)
2626
from git.compat import is_win
27-
from git.exc import HookExecutionError, InvalidGitRepositoryError
27+
from git.exc import (
28+
HookExecutionError,
29+
InvalidGitRepositoryError
30+
)
31+
from git.index.base import _FileStore
2832
from git.index.fun import hook_path
2933
from git.index.typ import BaseIndexEntry, IndexEntry
3034
from git.objects import Blob
@@ -953,4 +957,38 @@ def test_index_add_pathlike(self, rw_repo):
953957
file = git_dir / "file.txt"
954958
file.touch()
955959

956-
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)