summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules2
-rw-r--r--LICENSE.md2
-rw-r--r--README.md58
-rwxr-xr-xbin/dotbot2
-rw-r--r--dotbot/__init__.py2
-rw-r--r--dotbot/cli.py46
-rw-r--r--dotbot/dispatcher.py3
-rw-r--r--dotbot/messenger/messenger.py11
-rw-r--r--dotbot/plugins/__init__.py3
-rw-r--r--dotbot/plugins/clean.py (renamed from plugins/clean.py)6
-rw-r--r--dotbot/plugins/link.py (renamed from plugins/link.py)77
-rw-r--r--dotbot/plugins/shell.py (renamed from plugins/shell.py)0
m---------lib/pyyaml0
-rw-r--r--setup.cfg2
-rw-r--r--setup.py87
-rw-r--r--test/README.md41
-rw-r--r--test/Vagrantfile5
-rw-r--r--test/driver-lib.bash25
-rwxr-xr-xtest/test2
-rw-r--r--test/test-lib.bash12
-rwxr-xr-xtest/test_travis2
-rw-r--r--test/tests/clean-environment-variable-expansion.bash16
-rw-r--r--test/tests/find-python-executable.bash6
-rw-r--r--test/tests/link-glob-ambiguous.bash45
-rw-r--r--test/tests/link-glob-multi-star.bash31
-rw-r--r--test/tests/link-glob.bash47
-rw-r--r--test/tests/link-if.bash51
-rw-r--r--test/tests/plugin-dir.bash2
-rw-r--r--test/tests/plugin-disable-builtin.bash17
-rw-r--r--test/tests/plugin.bash2
-rw-r--r--test/tests/shim.bash29
-rwxr-xr-xtools/git-submodule/install1
33 files changed, 561 insertions, 77 deletions
diff --git a/.gitignore b/.gitignore
index 0d20b64..bf45d22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
+*.egg-info
*.pyc
+build/
+dist/
diff --git a/.gitmodules b/.gitmodules
index 111c39c..ffb9af9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,4 +1,4 @@
[submodule "lib/pyyaml"]
path = lib/pyyaml
- url = https://github.com/anishathalye/pyyaml
+ url = https://github.com/yaml/pyyaml
ignore = dirty
diff --git a/LICENSE.md b/LICENSE.md
index 432fcc0..1af799f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,7 +1,7 @@
The MIT License (MIT)
=====================
-**Copyright (c) 2014-2017 Anish Athalye (me@anishathalye.com)**
+**Copyright (c) 2014-2018 Anish Athalye (me@anishathalye.com)**
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/README.md b/README.md
index 0e9216c..2416d3a 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,17 @@ submodule; be sure to commit your changes before running `./install`, otherwise
the old version of Dotbot will be checked out by the install script. If using a
subrepo, run `git fetch && git checkout origin/master` in the Dotbot directory.
+If you prefer, you can install Dotbot from [PyPI] and call it as a command-line
+program:
+
+```bash
+pip install dotbot
+touch install.conf.yaml
+```
+
+In this case, rather than running `./install`, you can invoke Dotbot with
+`dotbot -c <path to configuration file>`.
+
### Full Example
Here's an example of a complete configuration.
@@ -165,17 +176,23 @@ files if necessary. Environment variables in paths are automatically expanded.
Link commands are specified as a dictionary mapping targets to source
locations. Source locations are specified relative to the base directory (that
-is specified when running the installer). Directory names should *not* contain
-a trailing "/" character.
+is specified when running the installer). If linking directories, *do not* include a trailing slash.
Link commands support an (optional) extended configuration. In this type of
configuration, instead of specifying source locations directly, targets are
-mapped to extended configuration dictionaries. These dictionaries map `path` to
-the source path, specify `create` as `true` if the parent directory should be
-created if necessary, specify `relink` as `true` if incorrect symbolic links
-should be automatically overwritten, specify `force` as `true` if the file or
-directory should be forcibly linked, and specify `relative` as `true` if the
-symbolic link should have a relative path.
+mapped to extended configuration dictionaries.
+
+Available extended configuration parameters:
+
+| Link Option | Explanation |
+| -- | -- |
+| `path` | The target 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) |
+| `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) |
+| `relative` | Use a relative path when creating the symlink (default:false, absolute links) |
+| `glob` | Treat a `*` character as a wildcard, and perform link operations on all of those matches (default:false) |
+| `if` | Execute this in your `$SHELL` and only link if it is successful. |
#### Example
@@ -207,6 +224,10 @@ the following three config files equivalent:
~/.zshrc:
force: true
path: zshrc
+ ~/.config/:
+ glob: true
+ path: config/*
+ relink: true
```
```yaml
@@ -217,6 +238,10 @@ the following three config files equivalent:
relink: true
~/.zshrc:
force: true
+ ~/.config/:
+ glob: true
+ path: config/*
+ relink: true
```
```json
@@ -230,6 +255,11 @@ the following three config files equivalent:
},
"~/.zshrc": {
"force": true
+ },
+ "~/.config/": {
+ "glob": true,
+ "path": "config/*",
+ "relink": true
}
}
}
@@ -350,12 +380,22 @@ Contributing
Do you have a feature request, bug report, or patch? Great! See
[CONTRIBUTING.md][contributing] for information on what you can do about that.
+Packaging
+---------
+
+1. Update version information.
+
+2. Build the package using ``python setup.py sdist bdist_wheel``.
+
+3. Sign and upload the package using ``twine upload -s dist/*``.
+
License
-------
-Copyright (c) 2014-2017 Anish Athalye. Released under the MIT License. See
+Copyright (c) 2014-2018 Anish Athalye. Released under the MIT License. See
[LICENSE.md][license] for details.
+[PyPI]: https://pypi.org/project/dotbot/
[init-dotfiles]: https://github.com/Vaelatern/init-dotfiles
[dotfiles-template]: https://github.com/anishathalye/dotfiles_template
[inspiration]: https://github.com/anishathalye/dotbot/wiki/Users
diff --git a/bin/dotbot b/bin/dotbot
index 78a0725..123fc93 100755
--- a/bin/dotbot
+++ b/bin/dotbot
@@ -11,7 +11,7 @@ which python3 >/dev/null 2>&1 && exec python3 "$0" "$@"
which python >/dev/null 2>&1 && exec python "$0" "$@"
which python2 >/dev/null 2>&1 && exec python2 "$0" "$@"
>&2 echo "error: cannot find python"
-return 1
+exit 1
'''
# python code
diff --git a/dotbot/__init__.py b/dotbot/__init__.py
index 1d03464..525e3cc 100644
--- a/dotbot/__init__.py
+++ b/dotbot/__init__.py
@@ -1,2 +1,4 @@
from .cli import main
from .plugin import Plugin
+
+__version__ = '1.14.1'
diff --git a/dotbot/cli.py b/dotbot/cli.py
index d77ab42..fdc2a13 100644
--- a/dotbot/cli.py
+++ b/dotbot/cli.py
@@ -7,25 +7,31 @@ from .messenger import Messenger
from .messenger import Level
from .util import module
+import dotbot
+import yaml
+
def add_options(parser):
- parser.add_argument('-Q', '--super-quiet', dest='super_quiet', action='store_true',
+ parser.add_argument('-Q', '--super-quiet', action='store_true',
help='suppress almost all output')
- parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
+ parser.add_argument('-q', '--quiet', action='store_true',
help='suppress most output')
- parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
+ parser.add_argument('-v', '--verbose', action='store_true',
help='enable verbose output')
- parser.add_argument('-d', '--base-directory', nargs=1,
- dest='base_directory', help='execute commands from within BASEDIR',
- metavar='BASEDIR', required=True)
- parser.add_argument('-c', '--config-file', nargs=1, dest='config_file',
- help='run commands given in CONFIGFILE', metavar='CONFIGFILE',
- required=True)
+ parser.add_argument('-d', '--base-directory',
+ help='execute commands from within BASEDIR',
+ metavar='BASEDIR')
+ parser.add_argument('-c', '--config-file',
+ help='run commands given in CONFIGFILE', metavar='CONFIGFILE')
parser.add_argument('-p', '--plugin', action='append', dest='plugins', default=[],
help='load PLUGIN as a plugin', metavar='PLUGIN')
- parser.add_argument('--disable-built-in-plugins', dest='disable_built_in_plugins',
+ parser.add_argument('--disable-built-in-plugins',
action='store_true', help='disable built-in plugins')
parser.add_argument('--plugin-dir', action='append', dest='plugin_dirs', default=[],
metavar='PLUGIN_DIR', help='load all plugins in PLUGIN_DIR')
+ parser.add_argument('--no-color', dest='no_color', action='store_true',
+ help='disable color output')
+ parser.add_argument('--version', action='store_true',
+ help='show program\'s version number and exit')
def read_config(config_file):
reader = ConfigReader(config_file)
@@ -37,15 +43,20 @@ def main():
parser = ArgumentParser()
add_options(parser)
options = parser.parse_args()
+ if options.version:
+ print('Dotbot version %s (yaml: %s)' % (dotbot.__version__, yaml.__version__))
+ exit(0)
if options.super_quiet:
log.set_level(Level.WARNING)
if options.quiet:
log.set_level(Level.INFO)
if options.verbose:
log.set_level(Level.DEBUG)
+ if options.no_color:
+ log.use_color(False)
plugin_directories = list(options.plugin_dirs)
if not options.disable_built_in_plugins:
- plugin_directories.append(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
+ from .plugins import Clean, Link, Shell
plugin_paths = []
for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, '*.py')):
@@ -55,10 +66,19 @@ def main():
for plugin_path in plugin_paths:
abspath = os.path.abspath(plugin_path)
module.load(abspath)
- tasks = read_config(options.config_file[0])
+ if not options.config_file:
+ log.error('No configuration file specified')
+ exit(1)
+ tasks = read_config(options.config_file)
if not isinstance(tasks, list):
raise ReadingError('Configuration file must be a list of tasks')
- dispatcher = Dispatcher(options.base_directory[0])
+ if options.base_directory:
+ base_directory = options.base_directory
+ else:
+ # default to directory of config file
+ base_directory = os.path.dirname(os.path.realpath(options.config_file))
+ os.chdir(base_directory)
+ dispatcher = Dispatcher(base_directory)
success = dispatcher.dispatch(tasks)
if success:
log.info('\n==> All tasks executed successfully')
diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py
index cc07435..d1a4f95 100644
--- a/dotbot/dispatcher.py
+++ b/dotbot/dispatcher.py
@@ -30,10 +30,11 @@ class Dispatcher(object):
try:
success &= plugin.handle(action, task[action])
handled = True
- except Exception:
+ except Exception as err:
self._log.error(
'An error was encountered while executing action %s' %
action)
+ self._log.debug(err)
if not handled:
success = False
self._log.error('Action %s not handled' % action)
diff --git a/dotbot/messenger/messenger.py b/dotbot/messenger/messenger.py
index f87a367..b83e3f2 100644
--- a/dotbot/messenger/messenger.py
+++ b/dotbot/messenger/messenger.py
@@ -7,10 +7,14 @@ from .level import Level
class Messenger(with_metaclass(Singleton, object)):
def __init__(self, level = Level.LOWINFO):
self.set_level(level)
+ self.use_color(True)
def set_level(self, level):
self._level = level
+ def use_color(self, yesno):
+ self._use_color = yesno
+
def log(self, level, message):
if (level >= self._level):
print('%s%s%s' % (self._color(level), message, self._reset()))
@@ -30,11 +34,14 @@ class Messenger(with_metaclass(Singleton, object)):
def error(self, message):
self.log(Level.ERROR, message)
+ def _should_use_color(self):
+ return self._use_color and sys.stdout.isatty()
+
def _color(self, level):
'''
Get a color (terminal escape sequence) according to a level.
'''
- if not sys.stdout.isatty():
+ if not self._should_use_color():
return ''
elif level < Level.DEBUG:
return ''
@@ -53,7 +60,7 @@ class Messenger(with_metaclass(Singleton, object)):
'''
Get a reset color (terminal escape sequence).
'''
- if not sys.stdout.isatty():
+ if not self._should_use_color():
return ''
else:
return Color.RESET
diff --git a/dotbot/plugins/__init__.py b/dotbot/plugins/__init__.py
new file mode 100644
index 0000000..93bd981
--- /dev/null
+++ b/dotbot/plugins/__init__.py
@@ -0,0 +1,3 @@
+from .clean import Clean
+from .link import Link
+from .shell import Shell
diff --git a/plugins/clean.py b/dotbot/plugins/clean.py
index 7e6cba1..22c975e 100644
--- a/plugins/clean.py
+++ b/dotbot/plugins/clean.py
@@ -34,11 +34,11 @@ class Clean(dotbot.Plugin):
Cleans all the broken symbolic links in target if they point to
a subdirectory of the base directory or if forced to clean.
'''
- if not os.path.isdir(os.path.expanduser(target)):
+ if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))):
self._log.debug('Ignoring nonexistent directory %s' % target)
return True
- for item in os.listdir(os.path.expanduser(target)):
- path = os.path.join(os.path.expanduser(target), item)
+ for item in os.listdir(os.path.expandvars(os.path.expanduser(target))):
+ path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item)
if not os.path.exists(path) and os.path.islink(path):
points_at = os.path.join(os.path.dirname(path), os.readlink(path))
if self._in_directory(path, self._context.base_directory()) or force:
diff --git a/plugins/link.py b/dotbot/plugins/link.py
index 4b50320..2506237 100644
--- a/plugins/link.py
+++ b/dotbot/plugins/link.py
@@ -1,6 +1,8 @@
import os
+import glob
import shutil
import dotbot
+import subprocess
class Link(dotbot.Plugin):
@@ -27,32 +29,86 @@ class Link(dotbot.Plugin):
force = defaults.get('force', False)
relink = defaults.get('relink', False)
create = defaults.get('create', False)
+ use_glob = defaults.get('glob', False)
+ test = defaults.get('if', None)
if isinstance(source, dict):
# extended config
+ test = source.get('if', test)
relative = source.get('relative', relative)
force = source.get('force', force)
relink = source.get('relink', relink)
create = source.get('create', create)
+ use_glob = source.get('glob', use_glob)
path = self._default_source(destination, source.get('path'))
else:
path = self._default_source(destination, source)
- path = os.path.expandvars(os.path.expanduser(path))
- if not self._exists(os.path.join(self._context.base_directory(), path)):
- success = False
- self._log.warning('Nonexistent target %s -> %s' %
- (destination, path))
+ if test is not None and not self._test_success(test):
+ self._log.lowinfo('Skipping %s' % destination)
continue
- if create:
- success &= self._create(destination)
- if force or relink:
- success &= self._delete(path, destination, relative, force)
- success &= self._link(path, destination, relative)
+ path = os.path.expandvars(os.path.expanduser(path))
+ if use_glob:
+ self._log.debug("Globbing with path: " + str(path))
+ glob_results = glob.glob(path)
+ if len(glob_results) is 0:
+ self._log.warning("Globbing couldn't find anything matching " + str(path))
+ success = False
+ continue
+ glob_star_loc = path.find('*')
+ if glob_star_loc is -1 and destination[-1] is '/':
+ self._log.error("Ambiguous action requested.")
+ self._log.error("No wildcard in glob, directory use undefined: " +
+ destination + " -> " + str(glob_results))
+ self._log.warning("Did you want to link the directory or into it?")
+ success = False
+ continue
+ elif glob_star_loc is -1 and len(glob_results) is 1:
+ # perform a normal link operation
+ if create:
+ success &= self._create(destination)
+ if force or relink:
+ success &= self._delete(path, destination, relative, force)
+ success &= self._link(path, destination, relative)
+ else:
+ self._log.lowinfo("Globs from '" + path + "': " + str(glob_results))
+ glob_base = path[:glob_star_loc]
+ for glob_full_item in glob_results:
+ glob_item = glob_full_item[len(glob_base):]
+ 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, force)
+ success &= self._link(glob_full_item, glob_link_destination, relative)
+ else:
+ if create:
+ success &= self._create(destination)
+ if not self._exists(os.path.join(self._context.base_directory(), path)):
+ success = False
+ self._log.warning('Nonexistent target %s -> %s' %
+ (destination, path))
+ continue
+ if force or relink:
+ success &= self._delete(path, destination, relative, force)
+ success &= self._link(path, destination, relative)
if success:
self._log.info('All links have been set up')
else:
self._log.error('Some links were not successfully set up')
return success
+ def _test_success(self, command):
+ with open(os.devnull, 'w') as devnull:
+ ret = subprocess.call(
+ command,
+ shell=True,
+ stdout=devnull,
+ stderr=devnull,
+ executable=os.environ.get('SHELL'),
+ )
+ if ret != 0:
+ self._log.debug('Test \'%s\' returned false' % command)
+ return ret == 0
+
def _default_source(self, destination, source):
if source is None:
basename = os.path.basename(destination)
@@ -87,6 +143,7 @@ class Link(dotbot.Plugin):
success = True
parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir))
if not self._exists(parent):
+ self._log.debug("Try to create parent: " + str(parent))
try:
os.makedirs(parent)
except OSError:
diff --git a/plugins/shell.py b/dotbot/plugins/shell.py
index 06a9a89..06a9a89 100644
--- a/plugins/shell.py
+++ b/dotbot/plugins/shell.py
diff --git a/lib/pyyaml b/lib/pyyaml
-Subproject f30c956c11aa6b5e7827fe5840cc9ed40b938d1
+Subproject 7e026bfee9cc0bddeb1bbca0c4a0bcd826c2bfd
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3c6e79c
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..975deb3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,87 @@
+from setuptools import setup, find_packages
+from codecs import open # For a consistent encoding
+from os import path
+import re
+
+
+here = path.dirname(__file__)
+
+
+with open(path.join(here, 'README.md'), encoding='utf-8') as f:
+ long_description = f.read()
+
+
+def read(*names, **kwargs):
+ with open(
+ path.join(here, *names),
+ encoding=kwargs.get("encoding", "utf8")
+ ) as fp:
+ return fp.read()
+
+
+def find_version(*file_paths):
+ version_file = read(*file_paths)
+ version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
+ version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError("Unable to find version string.")
+
+
+setup(
+ name='dotbot',
+
+ version=find_version('dotbot', '__init__.py'),
+
+ description='A tool that bootstraps your dotfiles',
+ long_description=long_description,
+ long_description_content_type='text/markdown',
+
+ url='https://github.com/anishathalye/dotbot',
+
+ author='Anish Athalye',
+ author_email='me@anishathalye.com',
+
+ license='MIT',
+
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+
+ 'Intended Audience :: Developers',
+
+ 'License :: OSI Approved :: MIT License',
+
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+
+ 'Topic :: Utilities',
+ ],
+
+ keywords='dotfiles',
+
+ packages=find_packages(),
+
+ setup_requires=[
+ 'setuptools>=38.6.0',
+ 'wheel>=0.31.0',
+ ],
+
+ install_requires=[
+ 'PyYAML>=3.12,<4',
+ ],
+
+ # To provide executable scripts, use entry points in preference to the
+ # "scripts" keyword. Entry points provide cross-platform support and allow
+ # pip to create the appropriate form of executable for the target platform.
+ entry_points={
+ 'console_scripts': [
+ 'dotbot=dotbot:main',
+ ],
+ },
+)
diff --git a/test/README.md b/test/README.md
index c4abddc..993a753 100644
--- a/test/README.md
+++ b/test/README.md
@@ -1,12 +1,37 @@
Testing
=======
-Dotbot testing code uses [Vagrant][vagrant] to run all tests inside a virtual
-machine to have tests be completely isolated from the host machine. The test
-driver relies on the [Sahara][sahara] plugin to snapshot and roll back virtual
-machine state. The tests are deterministic, and each test is run in a virtual
-machine with fresh state, ensuring that tests that modify system state are
-easily repeatable.
+Dotbot testing code uses [Vagrant] to run all tests inside a virtual machine to
+have tests be completely isolated from the host machine.
+
+Installing the Test environnement
+---------------------------------
+
+### Debian-based distributions
+
+- Install the test requirements
+
+```bash
+sudo apt install vagrant virtualbox
+```
+
+- Install Dotbot dependencies
+
+```bash
+git submodule update --init --recursive
+```
+
+### macOS
+
+- Install the test requirements
+ - [VirtualBox]
+ - [Vagrant]
+
+- Install Dotbot dependencies
+
+```bash
+git submodule update --init --recursive
+```
Running the Tests
-----------------
@@ -23,5 +48,5 @@ Tests can be run with a specific Python version by running `./test --version
When finished with testing, it is good to shut down the virtual machine by
running `vagrant halt`.
-[vagrant]: https://www.vagrantup.com/
-[sahara]: https://github.com/jedi4ever/sahara
+[VirtualBox]: https://www.virtualbox.org/wiki/Downloads
+[Vagrant]: https://www.vagrantup.com/
diff --git a/test/Vagrantfile b/test/Vagrantfile
index 6d3feb0..05d6747 100644
--- a/test/Vagrantfile
+++ b/test/Vagrantfile
@@ -1,9 +1,8 @@
Vagrant.configure(2) do |config|
- config.vm.box = 'debian/jessie64'
+ config.vm.box = 'debian/stretch64'
# sync by copying for isolation
- config.vm.synced_folder "..", "/dotbot", type: "rsync",
- rsync__exclude: ".git/"
+ config.vm.synced_folder "..", "/dotbot", type: "rsync"
# disable default synced folder
config.vm.synced_folder ".", "/vagrant", disabled: true
diff --git a/test/driver-lib.bash b/test/driver-lib.bash
index 56a0740..02a71a5 100644
--- a/test/driver-lib.bash
+++ b/test/driver-lib.bash
@@ -31,10 +31,6 @@ check_prereqs() {
>&2 echo "vagrant vm must be running."
return 1
fi
- if ! (vagrant plugin list | grep '^sahara\s\+') >/dev/null 2>&1; then
- >&2 echo "vagrant plugin 'sahara' is not installed."
- return 1
- fi
}
until_success() {
@@ -56,23 +52,26 @@ wait_for_vagrant() {
until_success vagrant ssh -c 'exit'
}
-rollback() {
- vagrant sandbox rollback >/dev/null 2>&1 &&
- wait_for_vagrant &&
- vagrant rsync >/dev/null 2>&1
+cleanup() {
+ vagrant ssh -c "
+ find . -not \\( \
+ -path './.pyenv' -o \
+ -path './.pyenv/*' -o \
+ -path './.bashrc' -o \
+ -path './.profile' -o \
+ -path './.ssh' -o \
+ -path './.ssh/*' \
+ \\) -delete" >/dev/null 2>&1
}
initialize() {
echo "initializing."
- vagrant sandbox on >/dev/null 2>&1
if ! vagrant ssh -c "pyenv local ${2}" >/dev/null 2>&1; then
- wait_for_vagrant && vagrant sandbox rollback >/dev/null 2>&1
- wait_for_vagrant
if ! vagrant ssh -c "pyenv install -s ${2} && pyenv local ${2}" >/dev/null 2>&1; then
die "could not install python ${2}"
fi
- vagrant sandbox commit >/dev/null 2>&1
fi
+ vagrant rsync >/dev/null 2>&1
tests_run=0
tests_passed=0
tests_failed=0
@@ -96,7 +95,7 @@ fail() {
run_test() {
tests_run=$((tests_run + 1))
printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}"
- rollback || die "unable to rollback vm." # start with a clean slate
+ cleanup
vagrant ssh -c "pyenv local ${2}" >/dev/null 2>&1
if vagrant ssh -c "cd /dotbot/test/tests && bash ${1}" 2>/dev/null; then
pass
diff --git a/test/test b/test/test
index e22f7fb..c018c32 100755
--- a/test/test
+++ b/test/test
@@ -24,7 +24,7 @@ do
;;
esac
done
-VERSION="${VERSION:-2.7.9}"
+VERSION="${VERSION:-3.6.4}"
declare -a tests=()
diff --git a/test/test-lib.bash b/test/test-lib.bash
index affb5c9..e4d9a4e 100644
--- a/test/test-lib.bash
+++ b/test/test-lib.bash
@@ -1,6 +1,6 @@
DEBUG=${DEBUG:-false}
USE_VAGRANT=${USE_VAGRANT:-true}
-DOTBOT_EXEC=${DOTBOT_EXEC:-"/dotbot/bin/dotbot"}
+DOTBOT_EXEC=${DOTBOT_EXEC:-"python /dotbot/bin/dotbot"}
DOTFILES="/home/$(whoami)/dotfiles"
INSTALL_CONF='install.conf.yaml'
INSTALL_CONF_JSON='install.conf.json'
@@ -47,17 +47,15 @@ initialize() {
run_dotbot() {
(
- cd "${DOTFILES}"
- cat > "${INSTALL_CONF}"
- ${DOTBOT_EXEC} -d . -c "${INSTALL_CONF}" "${@}"
+ cat > "${DOTFILES}/${INSTALL_CONF}"
+ ${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF}" "${@}"
)
}
run_dotbot_json() {
(
- cd "${DOTFILES}"
- cat > "${INSTALL_CONF_JSON}"
- ${DOTBOT_EXEC} -d . -c "${INSTALL_CONF_JSON}" "${@}"
+ cat > "${DOTFILES}/${INSTALL_CONF_JSON}"
+ ${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF_JSON}" "${@}"
)
}
diff --git a/test/test_travis b/test/test_travis
index 20ec1ae..79439e1 100755
--- a/test/test_travis
+++ b/test/test_travis
@@ -6,7 +6,7 @@ set -e
# set -x
# set -v
-BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+export BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Prevent execution outside of Travis CI builds
if [[ "${TRAVIS}" != true || "${CI}" != true ]]; then
diff --git a/test/tests/clean-environment-variable-expansion.bash b/test/tests/clean-environment-variable-expansion.bash
new file mode 100644
index 0000000..fedab45
--- /dev/null
+++ b/test/tests/clean-environment-variable-expansion.bash
@@ -0,0 +1,16 @@
+test_description='clean expands environment variables'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+ln -s ${DOTFILES}/f ~/.f
+'
+
+test_expect_success 'run' '
+run_dotbot <<EOF
+- clean: ["\$HOME"]
+EOF
+'
+
+test_expect_success 'test' '
+! test -h ~/.f
+'
diff --git a/test/tests/find-python-executable.bash b/test/tests/find-python-executable.bash
index cc46724..d4fa7eb 100644
--- a/test/tests/find-python-executable.bash
+++ b/test/tests/find-python-executable.bash
@@ -1,6 +1,10 @@
test_description='can find python executable with different names'
. '../test-lib.bash'
+if ${USE_VAGRANT}; then
+ DOTBOT_EXEC="/dotbot/bin/dotbot" # revert to calling it as a shell script
+fi
+
# the test machine needs to have a binary named `python`
test_expect_success 'setup' '
mkdir ~/tmp_bin &&
@@ -26,7 +30,7 @@ test_expect_success 'setup 2' '
touch ~/tmp_bin/python &&
chmod +x ~/tmp_bin/python &&
cat >> ~/tmp_bin/python <<EOF
-#!$HOME/tmp_bin/sh
+#!$HOME/tmp_bin/bash
exec $(which python)
EOF
'
diff --git a/test/tests/link-glob-ambiguous.bash b/test/tests/link-glob-ambiguous.bash
new file mode 100644
index 0000000..7e23348
--- /dev/null
+++ b/test/tests/link-glob-ambiguous.bash
@@ -0,0 +1,45 @@
+test_description='link glob ambiguous'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+mkdir ${DOTFILES}/foo
+'
+
+test_expect_failure 'run 1' '
+run_dotbot <<EOF
+- link:
+ ~/foo/:
+ path: foo
+ glob: true
+EOF
+'
+
+test_expect_failure 'test 1' '
+test -d ~/foo
+'
+
+test_expect_failure 'run 2' '
+run_dotbot <<EOF
+- link:
+ ~/foo/:
+ path: foo/
+ glob: true
+EOF
+'
+
+test_expect_failure 'test 2' '
+test -d ~/foo
+'
+
+test_expect_success 'run 3' '
+run_dotbot <<EOF
+- link:
+ ~/foo:
+ path: foo
+ glob: true
+EOF
+'
+
+test_expect_success 'test 3' '
+test -d ~/foo
+'
diff --git a/test/tests/link-glob-multi-star.bash b/test/tests/link-glob-multi-star.bash
new file mode 100644
index 0000000..11ae740
--- /dev/null
+++ b/test/tests/link-glob-multi-star.bash
@@ -0,0 +1,31 @@
+test_description='link glob'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+mkdir ${DOTFILES}/config &&
+mkdir ${DOTFILES}/config/foo &&
+mkdir ${DOTFILES}/config/bar &&
+echo "apple" > ${DOTFILES}/config/foo/a &&
+echo "banana" > ${DOTFILES}/config/bar/b &&
+echo "cherry" > ${DOTFILES}/config/bar/c
+'
+
+test_expect_success 'run' '
+run_dotbot -v <<EOF
+- defaults:
+ link:
+ glob: true
+ create: true
+- link:
+ ~/.config/: config/*/*
+EOF
+'
+
+test_expect_success 'test' '
+! readlink ~/.config/ &&
+! readlink ~/.config/foo &&
+readlink ~/.config/foo/a &&
+grep "apple" ~/.config/foo/a &&
+grep "banana" ~/.config/bar/b &&
+grep "cherry" ~/.config/bar/c
+'
diff --git a/test/tests/link-glob.bash b/test/tests/link-glob.bash
new file mode 100644
index 0000000..f1c813d
--- /dev/null
+++ b/test/tests/link-glob.bash
@@ -0,0 +1,47 @@
+test_description='link glob'
+. '../test-lib.bash'
+
+test_expect_success 'setup 1' '
+mkdir ${DOTFILES}/bin &&
+echo "apple" > ${DOTFILES}/bin/a &&
+echo "banana" > ${DOTFILES}/bin/b &&
+echo "cherry" > ${DOTFILES}/bin/c
+'
+
+test_expect_success 'run 1' '
+run_dotbot -v <<EOF
+- defaults:
+ link:
+ glob: true
+ create: true
+- link:
+ ~/bin: bin/*
+EOF
+'
+
+test_expect_success 'test 1' '
+grep "apple" ~/bin/a &&
+grep "banana" ~/bin/b &&
+grep "cherry" ~/bin/c
+'
+
+test_expect_success 'setup 2' '
+rm -rf ~/bin
+'
+
+test_expect_success 'run 2' '
+run_dotbot -v <<EOF
+- defaults:
+ link:
+ glob: true
+ create: true
+- link:
+ ~/bin/: bin/*
+EOF
+'
+
+test_expect_success 'test 2' '
+grep "apple" ~/bin/a &&
+grep "banana" ~/bin/b &&
+grep "cherry" ~/bin/c
+'
diff --git a/test/tests/link-if.bash b/test/tests/link-if.bash
new file mode 100644
index 0000000..1ea7709
--- /dev/null
+++ b/test/tests/link-if.bash
@@ -0,0 +1,51 @@
+test_description='link if'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+mkdir ~/d
+echo "apple" > ${DOTFILES}/f
+'
+
+test_expect_success 'run' '
+run_dotbot <<EOF
+- link:
+ ~/.f:
+ path: f
+ if: "true"
+ ~/.g:
+ path: f
+ if: "false"
+ ~/.h:
+ path: f
+ if: "[[ -d ~/d ]]"
+ ~/.i:
+ path: f
+ if: "badcommand"
+EOF
+'
+
+test_expect_success 'test' '
+grep "apple" ~/.f &&
+! test -f ~/.g &&
+grep "apple" ~/.h &&
+! test -f ~/.i
+'
+
+test_expect_success 'run 2' '
+run_dotbot <<EOF
+- defaults:
+ link:
+ if: "false"
+- link:
+ ~/.j:
+ path: f
+ if: "true"
+ ~/.k:
+ path: f
+EOF
+'
+
+test_expect_success 'test 2' '
+grep "apple" ~/.j &&
+! test -f ~/.k
+'
diff --git a/test/tests/plugin-dir.bash b/test/tests/plugin-dir.bash
index 299f144..f3a5e94 100644
--- a/test/tests/plugin-dir.bash
+++ b/test/tests/plugin-dir.bash
@@ -19,7 +19,7 @@ EOF
'
test_expect_success 'run' '
-run_dotbot --plugin-dir plugins <<EOF
+run_dotbot --plugin-dir ${DOTFILES}/plugins <<EOF
- test: ~
EOF
'
diff --git a/test/tests/plugin-disable-builtin.bash b/test/tests/plugin-disable-builtin.bash
new file mode 100644
index 0000000..f469b0f
--- /dev/null
+++ b/test/tests/plugin-disable-builtin.bash
@@ -0,0 +1,17 @@
+test_description='can disable built-in plugins'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+echo "apple" > ${DOTFILES}/f
+'
+
+test_expect_failure 'run' '
+run_dotbot --disable-built-in-plugins <<EOF
+- link:
+ ~/.f: f
+EOF
+'
+
+test_expect_failure 'test' '
+test -f ~/.f
+'
diff --git a/test/tests/plugin.bash b/test/tests/plugin.bash
index 960e9ce..bdf0c7f 100644
--- a/test/tests/plugin.bash
+++ b/test/tests/plugin.bash
@@ -18,7 +18,7 @@ EOF
'
test_expect_success 'run' '
-run_dotbot --plugin test.py <<EOF
+run_dotbot --plugin ${DOTFILES}/test.py <<EOF
- test: ~
EOF
'
diff --git a/test/tests/shim.bash b/test/tests/shim.bash
new file mode 100644
index 0000000..2ed7d54
--- /dev/null
+++ b/test/tests/shim.bash
@@ -0,0 +1,29 @@
+test_description='install shim works'
+. '../test-lib.bash'
+
+test_expect_success 'setup' '
+cd ${DOTFILES}
+git init
+if ${USE_VAGRANT}; then
+ git submodule add /dotbot dotbot
+else
+ git submodule add ${BASEDIR} dotbot
+fi
+cp ./dotbot/tools/git-submodule/install .
+echo "pear" > ${DOTFILES}/foo
+'
+
+test_expect_success 'run' '
+cat > ${DOTFILES}/install.conf.yaml <<EOF
+- link:
+ ~/.foo: foo
+EOF
+if ! ${USE_VAGRANT}; then
+ sed -i "" "1 s/sh$/python/" ${DOTFILES}/dotbot/bin/dotbot
+fi
+${DOTFILES}/install
+'
+
+test_expect_success 'test' '
+grep "pear" ~/.foo
+'
diff --git a/tools/git-submodule/install b/tools/git-submodule/install
index b1baa33..5a7e72c 100755
--- a/tools/git-submodule/install
+++ b/tools/git-submodule/install
@@ -9,6 +9,7 @@ DOTBOT_BIN="bin/dotbot"
BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${BASEDIR}"
+git -C "${DOTBOT_DIR}" submodule sync --quiet --recursive
git submodule update --init --recursive "${DOTBOT_DIR}"
"${BASEDIR}/${DOTBOT_DIR}/${DOTBOT_BIN}" -d "${BASEDIR}" -c "${CONFIG}" "${@}"