Skip to content

Commit 686db86

Browse files
committed
Merge branch 'kurtmckee/prevent-source-deletio...'
2 parents d89b11e + 733bb82 commit 686db86

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

src/dotbot/plugins/link.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ def _delete(self, source: str, path: str, *, relative: bool, canonical_path: boo
197197
success = True
198198
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
199199
fullpath = os.path.abspath(os.path.expanduser(path))
200+
if self._exists(path) and not self._is_link(path) and os.path.realpath(fullpath) == source:
201+
# Special case: The path is not a symlink but resolves to the source anyway.
202+
# Deleting the path would actually delete the source.
203+
# This may happen if a parent directory is a symlink.
204+
self._log.warning(f"{path} appears to be the same file as {source}.")
205+
return False
200206
if relative:
201207
source = self._relative_path(source, fullpath)
202208
if (self._is_link(path) and self._link_destination(path) != source) or (

tests/test_link.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import pathlib
23
import sys
34
from typing import Callable, Optional
45

@@ -962,6 +963,50 @@ def test_link_relink_relative_leaves_file(home: str, dotfiles: Dotfiles, run_dot
962963
assert mtime == new_mtime
963964

964965

966+
def test_source_is_not_overwritten_by_symlink_trickery(
967+
capsys: pytest.CaptureFixture[str], home: str, dotfiles: Dotfiles, run_dotbot: Callable[..., None]
968+
) -> None:
969+
dotfiles_path = pathlib.Path(dotfiles.directory)
970+
home_path = pathlib.Path(home)
971+
972+
# Setup:
973+
# * A symlink exists from `~/.ssh` to `ssh` in the dotfiles directory.
974+
# * Dotbot is configured to force-recreate a symlink between two files
975+
# when, in reality, it's actually the same file when resolved.
976+
ssh_config = (dotfiles_path / "ssh/config").absolute()
977+
os.mkdir(str(ssh_config.parent))
978+
ssh_config.write_text("preserve me!")
979+
os.symlink(str(ssh_config.parent), str(home_path / ".ssh"))
980+
dotfiles.write_config(
981+
[
982+
{
983+
"defaults": {
984+
"link": {
985+
"relink": True,
986+
"create": True,
987+
"force": True,
988+
},
989+
}
990+
},
991+
{
992+
"link": {
993+
# When symlinks are resolved, these are actually the same file.
994+
"~/.ssh/config": "ssh/config",
995+
},
996+
},
997+
]
998+
)
999+
1000+
# Execute dotbot.
1001+
with pytest.raises(SystemExit):
1002+
run_dotbot()
1003+
1004+
stdout, _ = capsys.readouterr()
1005+
assert "appears to be the same file" in stdout
1006+
# Verify that the file was not overwritten.
1007+
assert ssh_config.read_text() == "preserve me!"
1008+
1009+
9651010
def test_link_defaults_1(home: str, dotfiles: Dotfiles, run_dotbot: Callable[..., None]) -> None:
9661011
"""Verify that link doesn't overwrite non-dotfiles links by default."""
9671012

0 commit comments

Comments
 (0)