diff --git a/swh/loader/svn/ra.py b/swh/loader/svn/ra.py --- a/swh/loader/svn/ra.py +++ b/swh/loader/svn/ra.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2020 The Software Heritage developers +# Copyright (C) 2016-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -302,8 +302,14 @@ shutil.rmtree(fpath) else: os.remove(fpath) - if path in EOL_STYLE: - del EOL_STYLE[path] + + # when deleting a directory ensure to remove any eol style setting for the + # file it contains as they can be added again later in another revision + # without the svn:eol-style property set + fullpath = os.path.join(self.rootpath, path) + for eol_path in list(EOL_STYLE): + if eol_path.startswith(fullpath): + del EOL_STYLE[eol_path] def update_checksum(self): raise NotImplementedError("This should be implemented.") diff --git a/swh/loader/svn/tests/test_loader.py b/swh/loader/svn/tests/test_loader.py --- a/swh/loader/svn/tests/test_loader.py +++ b/swh/loader/svn/tests/test_loader.py @@ -3,11 +3,13 @@ # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information +from io import BytesIO import os import subprocess import pytest -from subvertpy import SubversionException +from subvertpy import SubversionException, delta, repos +from subvertpy.ra import Auth, RemoteAccess, get_username_provider from swh.loader.svn.loader import ( SvnLoader, @@ -786,3 +788,73 @@ "skipped_content": 0, "snapshot": 1, } + + +def test_loader_eol_style_file_property_handling_edge_case(swh_storage, tmp_path): + # create a repository + repo_path = os.path.join(tmp_path, "tmprepo") + repos.create(repo_path) + + # connect to the "remote" repository using the file transport. + repo_url = f"file://{repo_path}" + conn = RemoteAccess(repo_url, auth=Auth([get_username_provider()])) + + # first commit + with conn.get_commit_editor( + { + "svn:log": ( + "Add a directory containing a file with CRLF end of line " + "and set svn:eol-style property to native so CRLF will be " + "replaced by LF in the file when exporting the revision" + ) + } + ) as editor: + with editor.open_root() as root: + root.add_directory("directory").close() + with root.add_file("directory/file_with_crlf_eol.txt") as file: + file.change_prop("svn:eol-style", "native") + txdelta = file.apply_textdelta() + delta.send_stream(BytesIO(b"Hello world!\r\n"), txdelta) + + # second commit + with conn.get_commit_editor( + {"svn:log": "Remove previously added directory and file"} + ) as editor: + with editor.open_root() as root: + root.delete_entry("directory") + + # third commit + with conn.get_commit_editor( + { + "svn:log": ( + "Add again same directory containing same file with CRLF end of line " + "but do not set svn:eol-style property value so CRLF will not be " + "replaced by LF when exporting the revision" + ) + } + ) as editor: + with editor.open_root() as root: + root.add_directory("directory").close() + with root.add_file("directory/file_with_crlf_eol.txt") as file: + txdelta = file.apply_textdelta() + delta.send_stream(BytesIO(b"Hello world!\r\n"), txdelta) + + # instantiate a svn loader checking after each processed revision that + # the repository filesystem it reconstructed does not differ from a subversion + # export of that revision + loader = SvnLoader( + swh_storage, repo_url, destination_path=tmp_path, check_revision=1 + ) + + assert loader.load() == {"status": "eventful"} + assert loader.visit_status() == "full" + assert get_stats(loader.storage) == { + "content": 2, + "directory": 5, + "origin": 1, + "origin_visit": 1, + "release": 0, + "revision": 3, + "skipped_content": 0, + "snapshot": 1, + }