diff options
-rw-r--r-- | dotfiles/repository.py | 38 | ||||
-rw-r--r-- | tests/test_repository.py | 111 |
2 files changed, 94 insertions, 55 deletions
diff --git a/dotfiles/repository.py b/dotfiles/repository.py index a29c142..ac0be2c 100644 --- a/dotfiles/repository.py +++ b/dotfiles/repository.py @@ -1,5 +1,8 @@ +import os + from click import echo from pathlib import Path +from fnmatch import fnmatch from operator import attrgetter from .dotfile import Dotfile @@ -62,10 +65,8 @@ class Repository(object): return '<Repository %r>' % str(self.path) def _ignore(self, path): - if not path.is_file(): - return True for pattern in self.ignore_patterns: - if path.match(pattern): + if fnmatch(path, '*/%s' % pattern): return True return False @@ -96,20 +97,24 @@ class Repository(object): target = self._dotfile_target(path) - if not path.fnmatch('%s/*' % self.homedir): + if not fnmatch(path, '%s/*' % self.homedir): raise NotRootedInHome(path) - if path.fnmatch('%s/*' % self.path): + if fnmatch(path, '%s/*' % self.path): raise InRepository(path) if self._ignore(target): raise TargetIgnored(path) - if path.check(dir=1): + if path.is_dir(): raise IsDirectory(path) return Dotfile(path, target) def _contents(self, dir): """Return all unignored files contained below a directory.""" - return [x for x in dir.rglob('*') if not self._ignore(x)] + + def skip(path): + return path.is_dir() or self._ignore(path) + + return [x for x in dir.rglob('*') if not skip(x)] def contents(self): """Return a list of dotfiles for each file in the repository.""" @@ -133,7 +138,7 @@ class Repository(object): paths = list(set(map(Path, paths))) for path in paths: - if path.check(dir=1): + if path.is_dir(): paths.extend(self._contents(path)) paths.remove(path) @@ -146,7 +151,7 @@ class Repository(object): return [d for d in map(construct, paths) if d is not None] - def prune(self): + def prune(self, debug=False): """Remove any empty directories in the repository. After a remove operation, there may be empty directories remaining. @@ -154,9 +159,14 @@ class Repository(object): so pruning must take place explicitly after such operations occur. """ - def filter(node): - return node.check(dir=1) and not self._ignore(node) + def skip(path): + return self._ignore(path) or path == str(self.path) + + dirs = reversed([dir for dir, subdirs, files in + os.walk(self.path) if not skip(dir)]) - for dir in self.path.visit(filter, lambda x: not self._ignore(x)): - if not len(dir.listdir()): - dir.remove() + for dir in dirs: + if not len(os.listdir(dir)): + if debug: + echo('PRUNE %s' % (dir)) + os.rmdir(dir) diff --git a/tests/test_repository.py b/tests/test_repository.py index ed8543c..cdf3c72 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -1,7 +1,8 @@ import pytest from pathlib import Path -from dotfiles.exceptions import NotRootedInHome +from dotfiles.exceptions import NotRootedInHome, TargetIgnored, \ + IsDirectory, InRepository from dotfiles.repository import Repository, \ REMOVE_LEADING_DOT, IGNORE_PATTERNS @@ -88,45 +89,73 @@ def test_dotfile_target(repo, path): def test_dotfile(repo): + with pytest.raises(NotRootedInHome): repo._dotfile(Path('/tmp/foo')) - # with pytest.raises(TargetIgnored): - # repo.ignore_patterns = ['.foo'] - # repo.remove_leading_dot = False - # repo._dotfile(py.path.local(repo.homedir.join('.foo'))) - # repo._dotfile(repo.homedir / '.foo') - -# with pytest.raises(TargetIgnored): -# repo.ignore_patterns = ['foo'] -# repo._dotfile(repo.homedir.join('.bar/foo')) -# with pytest.raises(IsDirectory): -# repo._dotfile(repo.homedir.ensure_dir('.config')) - -# # The repo fixture is parametrized, we can only expect InRepository -# # exception when the repository is contained in the home directory. -# if repo.path.dirname == repo.homedir.basename: -# with pytest.raises(InRepository): -# repo._dotfile(repo.path.join('.foo/bar')) - -# repo._dotfile(repo.homedir.join('.foo')) - - -# def test_dotfiles(repo): -# file = repo.homedir.join('.baz') -# dir = repo.homedir.ensure_dir('.dir') -# dir.ensure('foo/bat') -# dir.ensure('foo/buz') -# dir.ensure('bar') -# dir.ensure('boo') - -# dotfiles = repo.dotfiles([str(file), str(dir)]) -# assert len(dotfiles) == 5 - - -# def test_prune(repo): -# repo.path.ensure_dir('.a/a') -# repo.path.ensure_dir('.b/b/b/b') -# repo.path.ensure_dir('.c/c/c/c/c/c/c/c') - -# repo.prune() -# assert len(repo.path.listdir()) == 0 + + with pytest.raises(TargetIgnored): + repo.ignore_patterns = ['.foo'] + repo.remove_leading_dot = False + repo._dotfile(repo.homedir / '.foo') + + with pytest.raises(TargetIgnored): + repo.ignore_patterns = ['foo'] + repo._dotfile(repo.homedir / '.bar/foo') + + with pytest.raises(IsDirectory): + dir = repo.homedir / '.config' + dir.mkdir() + repo._dotfile(dir) + + # The repo fixture is parametrized, we can only expect InRepository + # exception when the repository is contained in the home directory. + if repo.path.parent == repo.homedir.name: + with pytest.raises(InRepository): + repo._dotfile(repo.path / '.foo/bar') + + +def test_dotfiles(repo): + + subdir_a = repo.homedir / '.dir' + subdir_b = repo.homedir / '.dir/foo' + + for subdir in [subdir_a, subdir_b]: + subdir.mkdir() + + file_a = repo.homedir / '.baz' + file_b = subdir_a / 'bar' + file_c = subdir_a / 'boo' + file_d = subdir_b / 'bat' + file_e = subdir_b / 'buz' + + for file in [file_a, file_b, file_c, file_d, file_e]: + file.touch() + + dotfiles = repo.dotfiles([str(file_a), str(subdir_a)]) + + assert len(dotfiles) == 5 + + for file in [file_a, file_b, file_c, file_d, file_e]: + assert str(file) in map(str, dotfiles) + + +def test_prune(repo): + dir_a = repo.path / '.a/a' + dir_b = repo.path / '.b/b/b/b' + dir_c = repo.path / '.c/c/c/c/c/c/c/c' + + for dir in [dir_a, dir_b, dir_c]: + dir.mkdir(parents=True) + + repo.prune() + contents = [x for x in repo.path.rglob('*')] + assert len(contents) == 0 + + # verify ignored directories are excluded from pruning + dir_d = repo.path / '.git' + dir_d.mkdir() + + repo.prune() + contents = [x for x in repo.path.rglob('*')] + assert str(dir_d) in map(str, contents) + assert len(contents) == 1 |