mirror of
1
0
Fork 0

Compare commits

...

5 Commits

Author SHA1 Message Date
Anish Athalye b04a3f1844 Release 1.19.2 2023-07-09 16:28:06 -04:00
Anish Athalye 416f32f5fe Fix globbing when there is a single match
See https://github.com/anishathalye/dotbot/issues/282 and
https://github.com/anishathalye/dotbot/issues/315.

This patch simplifies the implementation, removing special-case handling
for the cases of zero matches and one match. Instead, any situation
where `glob: true` is specified and the path contains a glob character
(any of "?", "*", or "[") is treated as a glob case. The reason we check
both `use_glob` and `_has_glob_chars()` is to more gracefully handle the
case where the user has enabled globs by default, but most links do not
contain glob characters and should not be treated as globs.
2023-07-09 16:24:13 -04:00
Anish Athalye 9f8fd76f32 Add note about exclude path interpretation
See https://github.com/anishathalye/dotbot/issues/292.
2023-07-09 16:20:27 -04:00
Anish Athalye 4daa065dc9 Allow empty glob
This is convenient and more flexible. See
https://github.com/anishathalye/dotbot/issues/284.
2023-07-09 16:20:27 -04:00
Anish Athalye ed60c62432 Improve output of '--version' 2023-07-09 15:26:46 -04:00
5 changed files with 66 additions and 73 deletions

View File

@ -201,6 +201,8 @@ When `glob: True`, Dotbot uses [glob.glob](https://docs.python.org/3/library/glo
However, due to the design of `glob.glob`, using a glob pattern such as `config/*`, will **not** match items that begin with `.`. To specifically capture items that being with `.`, you will need to include the `.` in the pattern, like this: `config/.*`.
When using glob with the `exclude:` option, the paths in the exclude paths should be relative to the base directory, same as the glob pattern itself. For example, if a glob pattern `vim/*` matches directories `vim/autoload`, `vim/ftdetect`, `vim/ftplugin`, and `vim/spell`, and you want to ignore the spell directory, then you should use `exclude: ["vim/spell"]` (not just `"spell"`).
#### Example
```yaml

View File

@ -1,4 +1,4 @@
from .cli import main
from .plugin import Plugin
__version__ = "1.19.1"
__version__ = "1.19.2"

View File

@ -95,7 +95,7 @@ def main():
["git", "rev-parse", "HEAD"],
cwd=os.path.dirname(os.path.abspath(__file__)),
stderr=devnull,
)
).decode("ascii")
hash_msg = " (git %s)" % git_hash[:10]
except (OSError, subprocess.CalledProcessError):
hash_msg = ""

View File

@ -59,64 +59,39 @@ class Link(Plugin):
self._log.lowinfo("Skipping %s" % destination)
continue
path = os.path.normpath(os.path.expandvars(os.path.expanduser(path)))
if use_glob:
if use_glob and self._has_glob_chars(path):
glob_results = self._create_glob_results(path, exclude_paths)
if len(glob_results) == 0:
self._log.warning("Globbing couldn't find anything matching " + str(path))
success = False
continue
if len(glob_results) == 1 and destination[-1] == "/":
self._log.error("Ambiguous action requested.")
self._log.error(
"No wildcard in glob, directory use undefined: "
+ destination
+ " -> "
+ str(glob_results)
self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
for glob_full_item in glob_results:
# Find common dirname between pattern and the item:
glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item]))
glob_item = (
glob_full_item
if len(glob_dirname) == 0
else glob_full_item[len(glob_dirname) + 1 :]
)
self._log.warning("Did you want to link the directory or into it?")
success = False
continue
elif len(glob_results) == 1 and destination[-1] != "/":
# perform a normal link operation
# Add prefix to basepath, if provided
if base_prefix:
glob_item = base_prefix + glob_item
# where is it going
glob_link_destination = os.path.join(destination, glob_item)
if create:
success &= self._create(destination)
success &= self._create(glob_link_destination)
if force or relink:
success &= self._delete(path, destination, relative, canonical_path, force)
success &= self._link(
path, destination, relative, canonical_path, ignore_missing
)
else:
self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
for glob_full_item in glob_results:
# Find common dirname between pattern and the item:
glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item]))
glob_item = (
glob_full_item
if len(glob_dirname) == 0
else glob_full_item[len(glob_dirname) + 1 :]
)
# Add prefix to basepath, if provided
if base_prefix:
glob_item = base_prefix + glob_item
# where is it going
glob_link_destination = os.path.join(destination, glob_item)
if create:
success &= self._create(glob_link_destination)
if force or relink:
success &= self._delete(
glob_full_item,
glob_link_destination,
relative,
canonical_path,
force,
)
success &= self._link(
success &= self._delete(
glob_full_item,
glob_link_destination,
relative,
canonical_path,
ignore_missing,
force,
)
success &= self._link(
glob_full_item,
glob_link_destination,
relative,
canonical_path,
ignore_missing,
)
else:
if create:
success &= self._create(destination)
@ -155,6 +130,9 @@ class Link(Plugin):
else:
return source
def _has_glob_chars(self, path):
return any(i in path for i in "?*[")
def _glob(self, path):
"""
Wrap `glob.glob` in a python agnostic way, catching errors in usage.

View File

@ -270,7 +270,7 @@ def test_link_glob_4(home, dotfiles, run_dotbot):
@pytest.mark.parametrize("path", ("foo", "foo/"))
def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot):
def test_link_glob_ignore_no_glob_chars(path, home, dotfiles, run_dotbot):
"""Verify ambiguous link globbing fails."""
dotfiles.makedirs("foo")
@ -286,28 +286,8 @@ def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot):
}
]
)
with pytest.raises(SystemExit):
run_dotbot()
assert not os.path.exists(os.path.join(home, "foo"))
def test_link_glob_ambiguous_success(home, dotfiles, run_dotbot):
"""Verify the case where ambiguous link globbing succeeds."""
dotfiles.makedirs("foo")
dotfiles.write_config(
[
{
"link": {
"~/foo": {
"path": "foo",
"glob": True,
}
}
}
]
)
run_dotbot()
assert os.path.islink(os.path.join(home, "foo"))
assert os.path.exists(os.path.join(home, "foo"))
@ -587,6 +567,39 @@ def test_link_glob_recursive(home, dotfiles, run_dotbot):
assert file.read() == "cherry"
def test_link_glob_no_match(home, dotfiles, run_dotbot):
"""Verify that a glob with no match doesn't raise an error."""
dotfiles.makedirs("foo")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/foo": "foo/*"}},
]
)
run_dotbot()
def test_link_glob_single_match(home, dotfiles, run_dotbot):
"""Verify linking works even when glob matches exactly one file."""
# regression test for https://github.com/anishathalye/dotbot/issues/282
dotfiles.write("foo/a", "apple")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/foo": "foo/*"}},
]
)
run_dotbot()
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
@pytest.mark.skipif(
"sys.platform[:5] == 'win32'",
reason="These if commands won't run on Windows",