Merge 22a7cbb6f7
into 8d94c6ec1a
This commit is contained in:
commit
310c72973c
3 changed files with 105 additions and 13 deletions
27
README.md
27
README.md
|
@ -175,19 +175,20 @@ Link commands support an optional extended configuration. In this type of
|
||||||
configuration, instead of specifying source locations directly, targets are
|
configuration, instead of specifying source locations directly, targets are
|
||||||
mapped to extended configuration dictionaries.
|
mapped to extended configuration dictionaries.
|
||||||
|
|
||||||
| Parameter | Explanation |
|
| Parameter | Explanation |
|
||||||
| --- | --- |
|
|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `path` | The source for the symlink, the same as in the shortcut syntax (default: null, automatic (see below)) |
|
| `path` | The source for the symlink, the same as in the shortcut syntax (default: null, automatic (see below)) |
|
||||||
| `create` | When true, create parent directories to the link as needed. (default: false) |
|
| `create` | When true, create parent directories to the link as needed. (default: false) |
|
||||||
| `relink` | Removes the old target if it's a symlink (default: false) |
|
| `relink` | Removes the old target if it's a symlink (default: false) |
|
||||||
| `force` | Force removes the old target, file or folder, and forces a new link (default: false) |
|
| `force` | Force removes the old target, file or folder, and forces a new link (default: false) |
|
||||||
| `relative` | Use a relative path to the source when creating the symlink (default: false, absolute links) |
|
| `backup` | Backup existing files/directories if they exist, creating a backup with suffix `.dotbot-backup (default: false) |
|
||||||
| `canonicalize` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default: true, real paths) |
|
| `relative` | Use a relative path to the source when creating the symlink (default: false, absolute links) |
|
||||||
| `if` | Execute this in your `$SHELL` and only link if it is successful. |
|
| `canonicalize` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default: true, real paths) |
|
||||||
| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default: false) |
|
| `if` | Execute this in your `$SHELL` and only link if it is successful. |
|
||||||
| `glob` | Treat `path` as a glob pattern, expanding patterns referenced below, linking all *files* matched. (default: false) |
|
| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default: false) |
|
||||||
| `exclude` | Array of glob patterns to remove from glob matches. Uses same syntax as `path`. Ignored if `glob` is `false`. (default: empty, keep all matches) |
|
| `glob` | Treat `path` as a glob pattern, expanding patterns referenced below, linking all *files* matched. (default: false) |
|
||||||
| `prefix` | Prepend prefix prefix to basename of each file when linked, when `glob` is `true`. (default: '') |
|
| `exclude` | Array of glob patterns to remove from glob matches. Uses same syntax as `path`. Ignored if `glob` is `false`. (default: empty, keep all matches) |
|
||||||
|
| `prefix` | Prepend prefix prefix to basename of each file when linked, when `glob` is `true`. (default: '') |
|
||||||
|
|
||||||
When `glob: True`, Dotbot uses [glob.glob](https://docs.python.org/3/library/glob.html#glob.glob) to resolve glob paths, expanding Unix shell-style wildcards, which are **not** the same as regular expressions; Only the following are expanded:
|
When `glob: True`, Dotbot uses [glob.glob](https://docs.python.org/3/library/glob.html#glob.glob) to resolve glob paths, expanding Unix shell-style wildcards, which are **not** the same as regular expressions; Only the following are expanded:
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Link(Plugin):
|
||||||
relink = defaults.get("relink", False)
|
relink = defaults.get("relink", False)
|
||||||
create = defaults.get("create", False)
|
create = defaults.get("create", False)
|
||||||
use_glob = defaults.get("glob", False)
|
use_glob = defaults.get("glob", False)
|
||||||
|
backup = defaults.get("backup", False)
|
||||||
base_prefix = defaults.get("prefix", "")
|
base_prefix = defaults.get("prefix", "")
|
||||||
test = defaults.get("if", None)
|
test = defaults.get("if", None)
|
||||||
ignore_missing = defaults.get("ignore-missing", False)
|
ignore_missing = defaults.get("ignore-missing", False)
|
||||||
|
@ -49,6 +50,7 @@ class Link(Plugin):
|
||||||
relink = source.get("relink", relink)
|
relink = source.get("relink", relink)
|
||||||
create = source.get("create", create)
|
create = source.get("create", create)
|
||||||
use_glob = source.get("glob", use_glob)
|
use_glob = source.get("glob", use_glob)
|
||||||
|
backup = source.get("backup", backup)
|
||||||
base_prefix = source.get("prefix", base_prefix)
|
base_prefix = source.get("prefix", base_prefix)
|
||||||
ignore_missing = source.get("ignore-missing", ignore_missing)
|
ignore_missing = source.get("ignore-missing", ignore_missing)
|
||||||
exclude_paths = source.get("exclude", exclude_paths)
|
exclude_paths = source.get("exclude", exclude_paths)
|
||||||
|
@ -85,6 +87,8 @@ class Link(Plugin):
|
||||||
canonical_path,
|
canonical_path,
|
||||||
force,
|
force,
|
||||||
)
|
)
|
||||||
|
if backup:
|
||||||
|
success &= self._backup(glob_link_destination)
|
||||||
success &= self._link(
|
success &= self._link(
|
||||||
glob_full_item,
|
glob_full_item,
|
||||||
glob_link_destination,
|
glob_link_destination,
|
||||||
|
@ -107,6 +111,8 @@ class Link(Plugin):
|
||||||
continue
|
continue
|
||||||
if force or relink:
|
if force or relink:
|
||||||
success &= self._delete(path, destination, relative, canonical_path, force)
|
success &= self._delete(path, destination, relative, canonical_path, force)
|
||||||
|
if backup:
|
||||||
|
success &= self._backup(destination)
|
||||||
success &= self._link(path, destination, relative, canonical_path, ignore_missing)
|
success &= self._link(path, destination, relative, canonical_path, ignore_missing)
|
||||||
if success:
|
if success:
|
||||||
self._log.info("All links have been set up")
|
self._log.info("All links have been set up")
|
||||||
|
@ -197,6 +203,21 @@ class Link(Plugin):
|
||||||
self._log.lowinfo("Creating directory %s" % parent)
|
self._log.lowinfo("Creating directory %s" % parent)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
def _backup(self, path):
|
||||||
|
success = True
|
||||||
|
if self._exists(path) and not self._is_link(path):
|
||||||
|
file_to_backup = os.path.join(os.path.expanduser(path))
|
||||||
|
backup_path = file_to_backup + ".dotbot-backup"
|
||||||
|
self._log.debug(f"Try to backup file {file_to_backup} to {backup_path}")
|
||||||
|
try:
|
||||||
|
os.rename(file_to_backup, backup_path)
|
||||||
|
except OSError:
|
||||||
|
self._log.warning(f"Failed to backup file {file_to_backup} to {backup_path}")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
self._log.lowinfo(f"Backed up file {file_to_backup} to {backup_path}")
|
||||||
|
return success
|
||||||
|
|
||||||
def _delete(self, source, path, relative, canonical_path, force):
|
def _delete(self, source, path, relative, canonical_path, force):
|
||||||
success = True
|
success = True
|
||||||
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
|
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
|
||||||
|
|
|
@ -175,6 +175,76 @@ def test_link_force_overwrite_symlink(home, dotfiles, run_dotbot):
|
||||||
assert os.path.isfile(os.path.join(home, ".dir", "f"))
|
assert os.path.isfile(os.path.join(home, ".dir", "f"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_is_created_if_destination_exists(home, dotfiles, run_dotbot):
|
||||||
|
"""Verify that the backup directory is created if destination exists."""
|
||||||
|
|
||||||
|
os.mkdir(os.path.join(home, ".dir"))
|
||||||
|
dotfiles.write("dir")
|
||||||
|
|
||||||
|
config = [{"link": {"~/.dir": {"path": "dir", "backup": True}}}]
|
||||||
|
dotfiles.write_config(config)
|
||||||
|
run_dotbot()
|
||||||
|
|
||||||
|
assert os.path.exists(os.path.join(home, ".dir.dotbot-backup"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_file_is_created_if_destination_exists(home, dotfiles, run_dotbot):
|
||||||
|
"""Verify that a backup file is created if destination exists."""
|
||||||
|
|
||||||
|
open(os.path.join(home, ".file"), "a").close()
|
||||||
|
dotfiles.write("file")
|
||||||
|
|
||||||
|
config = [{"link": {"~/.file": {"path": "file", "backup": True}}}]
|
||||||
|
dotfiles.write_config(config)
|
||||||
|
run_dotbot()
|
||||||
|
|
||||||
|
assert os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_file_not_created_if_link(home, dotfiles, run_dotbot):
|
||||||
|
"""Verify that a backup file isn't created if destination is a symlink."""
|
||||||
|
|
||||||
|
open(os.path.join(home, "file"), "a").close()
|
||||||
|
dotfiles.write("file")
|
||||||
|
os.symlink(os.path.join(home, "file"), os.path.join(home, ".file"))
|
||||||
|
|
||||||
|
config = [{"link": {"~/.file": {"path": "file", "backup": True}}}]
|
||||||
|
dotfiles.write_config(config)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
run_dotbot()
|
||||||
|
|
||||||
|
assert not os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_file_not_created_if_force(home, dotfiles, run_dotbot):
|
||||||
|
"""Verify that a backup file is created while using force option."""
|
||||||
|
|
||||||
|
open(os.path.join(home, ".file"), "a").close()
|
||||||
|
dotfiles.write("file")
|
||||||
|
|
||||||
|
config = [{"link": {"~/.file": {"path": "file", "backup": True, "force": True}}}]
|
||||||
|
dotfiles.write_config(config)
|
||||||
|
run_dotbot()
|
||||||
|
|
||||||
|
assert not os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_error_if_dest_already_exists(home, dotfiles, run_dotbot):
|
||||||
|
"""Verify an error is thrown if the backup already exists."""
|
||||||
|
|
||||||
|
os.mkdir(os.path.join(home, ".dir"))
|
||||||
|
os.mkdir(os.path.join(home, ".dir.dotbot-backup"))
|
||||||
|
open(os.path.join(home, ".dir.dotbot-backup", "f"), "a").close()
|
||||||
|
dotfiles.write("dir")
|
||||||
|
|
||||||
|
config = [{"link": {"~/.dir": {"path": "dir", "backup": True}}}]
|
||||||
|
dotfiles.write_config(config)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
run_dotbot()
|
||||||
|
|
||||||
|
assert os.path.exists(os.path.join(home, ".dir.dotbot-backup", "f"))
|
||||||
|
|
||||||
|
|
||||||
def test_link_glob_1(home, dotfiles, run_dotbot):
|
def test_link_glob_1(home, dotfiles, run_dotbot):
|
||||||
"""Verify globbing works."""
|
"""Verify globbing works."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue