Skip to content

Commit

Permalink
Avoid deleting the source file when linking
Browse files Browse the repository at this point in the history
  • Loading branch information
kurtmckee committed Jan 14, 2025
1 parent 9cd5d07 commit 733bb82
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/dotbot/plugins/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ def _delete(self, source: str, path: str, *, relative: bool, canonical_path: boo
success = True
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
fullpath = os.path.abspath(os.path.expanduser(path))
if self._exists(path) and not self._is_link(path) and os.path.realpath(fullpath) == source:
# Special case: The path is not a symlink but resolves to the source anyway.
# Deleting the path would actually delete the source.
# This may happen if a parent directory is a symlink.
self._log.warning(f"{path} appears to be the same file as {source}.")
return False
if relative:
source = self._relative_path(source, fullpath)
if (self._is_link(path) and self._link_destination(path) != source) or (
Expand Down
45 changes: 45 additions & 0 deletions tests/test_link.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pathlib
import sys
from typing import Callable, Optional

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


def test_source_is_not_overwritten_by_symlink_trickery(
capsys: pytest.CaptureFixture[str], home: str, dotfiles: Dotfiles, run_dotbot: Callable[..., None]
) -> None:
dotfiles_path = pathlib.Path(dotfiles.directory)
home_path = pathlib.Path(home)

# Setup:
# * A symlink exists from `~/.ssh` to `ssh` in the dotfiles directory.
# * Dotbot is configured to force-recreate a symlink between two files
# when, in reality, it's actually the same file when resolved.
ssh_config = (dotfiles_path / "ssh/config").absolute()
os.mkdir(str(ssh_config.parent))
ssh_config.write_text("preserve me!")
os.symlink(str(ssh_config.parent), str(home_path / ".ssh"))
dotfiles.write_config(
[
{
"defaults": {
"link": {
"relink": True,
"create": True,
"force": True,
},
}
},
{
"link": {
# When symlinks are resolved, these are actually the same file.
"~/.ssh/config": "ssh/config",
},
},
]
)

# Execute dotbot.
with pytest.raises(SystemExit):
run_dotbot()

stdout, _ = capsys.readouterr()
assert "appears to be the same file" in stdout
# Verify that the file was not overwritten.
assert ssh_config.read_text() == "preserve me!"


def test_link_defaults_1(home: str, dotfiles: Dotfiles, run_dotbot: Callable[..., None]) -> None:
"""Verify that link doesn't overwrite non-dotfiles links by default."""

Expand Down

0 comments on commit 733bb82

Please sign in to comment.