Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid deleting the source file when linking #373

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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