mirror of
1
0
Fork 0

Merge branch 'upstream_master'

This commit is contained in:
Andreas Schmidt 2020-02-18 17:56:50 +01:00
commit 58160cb1f3
No known key found for this signature in database
GPG Key ID: FEE0A611BEA6DEA0
24 changed files with 228 additions and 202 deletions

View File

@ -6,10 +6,11 @@ python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "nightly"
- "pypy3"
sudo: false
script:
- ./test/test_travis
- ./test/test

View File

@ -1,7 +1,7 @@
The MIT License (MIT)
=====================
**Copyright (c) 2014-2019 Anish Athalye (me@anishathalye.com)**
**Copyright (c) 2014-2020 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

View File

@ -1,4 +1,4 @@
# Dotbot [![Build Status](https://travis-ci.org/anishathalye/dotbot.svg?branch=master)](https://travis-ci.org/anishathalye/dotbot)
# Dotbot [![Build Status](https://travis-ci.com/anishathalye/dotbot.svg?branch=master)](https://travis-ci.com/anishathalye/dotbot)
Dotbot makes installing your dotfiles as easy as `git clone $url && cd dotfiles
&& ./install`, even on a freshly installed system!
@ -176,8 +176,10 @@ Available extended configuration parameters:
| `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 to the source when creating the symlink (default:false, absolute links) |
| `canonicalize-path` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default:true, real paths) |
| `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. |
| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default:false) |
#### Example
@ -302,7 +304,9 @@ Clean commands are specified as an array of directories to be cleaned.
Clean commands support an extended configuration syntax. In this type of
configuration, commands are specified as directory paths mapping to options. If
the `force` option is set to `true`, dead links are removed even if they don't
point to a file inside the dotfiles directory.
point to a file inside the dotfiles directory. If `recursive` is set to `true`,
the directory is traversed recursively (not recommended for `~` because it will
be slow).
#### Example
@ -310,8 +314,10 @@ point to a file inside the dotfiles directory.
- clean: ['~']
- clean:
~/.config:
~/:
force: true
~/.config:
recursive: true
```
### Defaults
@ -395,7 +401,7 @@ Do you have a feature request, bug report, or patch? Great! See
## License
Copyright (c) 2014-2019 Anish Athalye. Released under the MIT License. See
Copyright (c) 2014-2020 Anish Athalye. Released under the MIT License. See
[LICENSE.md][license] for details.
[PyPI]: https://pypi.org/project/dotbot/

View File

@ -1,4 +1,4 @@
from .cli import main
from .plugin import Plugin
__version__ = '1.16.0'
__version__ = '1.17.0'

View File

@ -73,10 +73,10 @@ def main():
if not isinstance(tasks, list):
raise ReadingError('Configuration file must be a list of tasks')
if options.base_directory:
base_directory = options.base_directory
base_directory = os.path.abspath(options.base_directory)
else:
# default to directory of config file
base_directory = os.path.dirname(os.path.realpath(options.config_file))
base_directory = os.path.dirname(os.path.abspath(options.config_file))
os.chdir(base_directory)
dispatcher = Dispatcher(base_directory)
success = dispatcher.dispatch(tasks)

View File

@ -1,4 +1,5 @@
import copy
import os
class Context(object):
'''
@ -13,8 +14,11 @@ class Context(object):
def set_base_directory(self, base_directory):
self._base_directory = base_directory
def base_directory(self):
return self._base_directory
def base_directory(self, canonical_path=True):
base_directory = self._base_directory
if canonical_path:
base_directory = os.path.realpath(base_directory)
return base_directory
def set_defaults(self, defaults):
self._defaults = defaults

View File

@ -10,8 +10,8 @@ class Dispatcher(object):
self._load_plugins()
def _setup_context(self, base_directory):
path = os.path.abspath(os.path.realpath(
os.path.expanduser(base_directory)))
path = os.path.abspath(
os.path.expanduser(base_directory))
if not os.path.exists(path):
raise DispatchError('Nonexistent base directory')
self._context = Context(path)

View File

@ -18,18 +18,20 @@ class Clean(dotbot.Plugin):
def _process_clean(self, targets):
success = True
defaults = self._context.defaults().get(self._directive, {})
force = defaults.get('force', False)
for target in targets:
if isinstance(targets, dict):
force = defaults.get('force', False)
recursive = defaults.get('recursive', False)
if isinstance(targets, dict) and isinstance(targets[target], dict):
force = targets[target].get('force', force)
success &= self._clean(target, force)
recursive = targets[target].get('recursive', recursive)
success &= self._clean(target, force, recursive)
if success:
self._log.info('All targets have been cleaned')
else:
self._log.error('Some targets were not successfully cleaned')
return success
def _clean(self, target, force):
def _clean(self, target, force, recursive):
'''
Cleans all the broken symbolic links in target if they point to
a subdirectory of the base directory or if forced to clean.
@ -39,6 +41,11 @@ class Clean(dotbot.Plugin):
return True
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 recursive and os.path.isdir(path):
# isdir implies not islink -- we don't want to descend into
# symlinked directories. okay to do a recursive call here
# because depth should be fairly limited
self._clean(path, force, recursive)
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:

View File

@ -26,19 +26,23 @@ class Link(dotbot.Plugin):
for destination, source in links.items():
destination = os.path.expandvars(destination)
relative = defaults.get('relative', False)
canonical_path = defaults.get('canonicalize-path', True)
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)
ignore_missing = defaults.get('ignore-missing', False)
if isinstance(source, dict):
# extended config
test = source.get('if', test)
relative = source.get('relative', relative)
canonical_path = source.get('canonicalize-path', canonical_path)
force = source.get('force', force)
relink = source.get('relink', relink)
create = source.get('create', create)
use_glob = source.get('glob', use_glob)
ignore_missing = source.get('ignore-missing', ignore_missing)
path = self._default_source(destination, source.get('path'))
else:
path = self._default_source(destination, source)
@ -49,25 +53,25 @@ class Link(dotbot.Plugin):
if use_glob:
self._log.debug("Globbing with path: " + str(path))
glob_results = glob.glob(path)
if len(glob_results) is 0:
if len(glob_results) == 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 '/':
if glob_star_loc == -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.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:
elif glob_star_loc == -1 and len(glob_results) == 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)
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))
glob_base = path[:glob_star_loc]
@ -77,19 +81,23 @@ class Link(dotbot.Plugin):
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)
success &= self._delete(glob_full_item, glob_link_destination, relative, canonical_path, force)
success &= self._link(glob_full_item, glob_link_destination, relative, canonical_path, ignore_missing)
else:
if create:
success &= self._create(destination)
if not self._exists(os.path.join(self._context.base_directory(), path)):
if not ignore_missing and not self._exists(os.path.join(self._context.base_directory(), path)):
# we seemingly check this twice (here and in _link) because
# if the file doesn't exist and force is True, we don't
# want to remove the original (this is tested by
# link-force-leaves-when-nonexistent.bash)
success = False
self._log.warning('Nonexistent source %s -> %s' %
(destination, path))
continue
if force or relink:
success &= self._delete(path, destination, relative, force)
success &= self._link(path, destination, relative)
success &= self._delete(path, destination, relative, canonical_path, force)
success &= self._link(path, destination, relative, canonical_path, ignore_missing)
if success:
self._log.info('All links have been set up')
else:
@ -153,9 +161,9 @@ class Link(dotbot.Plugin):
self._log.lowinfo('Creating directory %s' % parent)
return success
def _delete(self, source, path, relative, force):
def _delete(self, source, path, relative, canonical_path, force):
success = True
source = os.path.join(self._context.base_directory(), source)
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
fullpath = os.path.expanduser(path)
if relative:
source = self._relative_path(source, fullpath)
@ -189,7 +197,7 @@ class Link(dotbot.Plugin):
destination_dir = os.path.dirname(destination)
return os.path.relpath(source, destination_dir)
def _link(self, source, link_name, relative):
def _link(self, source, link_name, relative, canonical_path, ignore_missing):
'''
Links link_name to source.
@ -197,7 +205,8 @@ class Link(dotbot.Plugin):
'''
success = False
destination = os.path.expanduser(link_name)
absolute_source = os.path.join(self._context.base_directory(), source)
base_directory = self._context.base_directory(canonical_path=canonical_path)
absolute_source = os.path.join(base_directory, source)
if relative:
source = self._relative_path(absolute_source, destination)
else:
@ -209,7 +218,7 @@ class Link(dotbot.Plugin):
# we need to use absolute_source below because our cwd is the dotfiles
# directory, and if source is relative, it will be relative to the
# destination directory
elif not self._exists(link_name) and self._exists(absolute_source):
elif not self._exists(link_name) and (ignore_missing or self._exists(absolute_source)):
try:
os.symlink(source, destination)
except OSError:

@ -1 +1 @@
Subproject commit 0f64cbfa54b0b22dc7b776b7b98a7cd657e84d78
Subproject commit 2f463cf5b0e98a52bc20e348d1e69761bf263b86

View File

@ -73,7 +73,7 @@ setup(
],
install_requires=[
'PyYAML>=5.1.2,<6',
'PyYAML>=5.3,<6',
],
# To provide executable scripts, use entry points in preference to the

View File

@ -36,14 +36,24 @@ git submodule update --init --recursive
Running the Tests
-----------------
Before running the tests, the virtual machine must be running. It can be
started by running `vagrant up`.
Before running the tests, you must SSH into the VM. Start it with `vagrant up`
and SSH in with `vagrant ssh`. All following commands must be run inside the
VM.
The test suite can be run by running `./test`. Selected tests can be run by
passing paths to the tests as arguments to `./test`.
First, you must install a version of Python to test against, using `pyenv
install -s {version}`. You can choose any version you like, e.g. `3.8.1`. It
isn't particularly important to test against all supported versions of Python
in the VM, because they will be tested by CI. Once you've installed a specific
version of Python, activate it with `pyenv global {version}`.
Tests can be run with a specific Python version by running `./test --version
<version>` - for example, `./test --version 3.4.3`.
The VM mounts the Dotbot directory in `/dotbot` as read-only (you can make
edits on your host machine). You can run the test suite by `cd /dotbot/test`
and then running `./test`. Selected tests can be run by passing paths to the
tests as arguments, e.g. `./test tests/create.bash tests/defaults.bash`.
To debug tests, you can prepend the line `DEBUG=true` as the first line to any
individual test (a `.bash` file inside `test/tests`). This will enable printing
stdout/stderr.
When finished with testing, it is good to shut down the virtual machine by
running `vagrant halt`.

4
test/Vagrantfile vendored
View File

@ -1,8 +1,8 @@
Vagrant.configure(2) do |config|
config.vm.box = 'debian/buster64'
config.vm.box = 'ubuntu/bionic64'
# sync by copying for isolation
config.vm.synced_folder "..", "/dotbot", type: "rsync"
config.vm.synced_folder "..", "/dotbot", mount_options: ["ro"]
# disable default synced folder
config.vm.synced_folder ".", "/vagrant", disabled: true

View File

@ -1,6 +1,3 @@
MAXRETRY=5
TIMEOUT=1
red() {
if [ -t 1 ]; then
printf "\033[31m%s\033[0m\n" "$*"
@ -26,52 +23,35 @@ yellow() {
}
check_prereqs() {
if ! (vagrant ssh -c 'exit') >/dev/null 2>&1; then
>&2 echo "vagrant vm must be running."
return 1
check_env() {
if [[ "$(whoami)" != "vagrant" && ( "${TRAVIS}" != true || "${CI}" != true ) ]]; then
die "tests must be run inside Travis or Vagrant"
fi
}
until_success() {
local timeout=${TIMEOUT}
local attempt=0
while [ $attempt -lt $MAXRETRY ]; do
if ($@) >/dev/null 2>&1; then
return 0
fi
sleep $timeout
timeout=$((timeout * 2))
attempt=$((attempt + 1))
done
return 1
}
wait_for_vagrant() {
until_success vagrant ssh -c 'exit'
}
cleanup() {
vagrant ssh -c "
find . -not \\( \
(
if [ "$(whoami)" == "vagrant" ]; then
cd $HOME
find . -not \( \
-path './.pyenv' -o \
-path './.pyenv/*' -o \
-path './.bashrc' -o \
-path './.profile' -o \
-path './.ssh' -o \
-path './.ssh/*' \
\\) -delete" >/dev/null 2>&1
\) -delete >/dev/null 2>&1
else
find ~ -mindepth 1 -newermt "${date_stamp}" \
-not \( -path ~ -o -path "${BASEDIR}/*" \
-o -path ~/dotfiles \) \
-exec rm -rf {} +
fi
) || true
}
initialize() {
echo "initializing."
if ! vagrant ssh -c "pyenv local ${2}" >/dev/null 2>&1; then
if ! vagrant ssh -c "pyenv install -s ${2} && pyenv local ${2}" >/dev/null 2>&1; then
die "could not install python ${2}"
fi
fi
vagrant rsync >/dev/null 2>&1
tests_run=0
tests_passed=0
tests_failed=0
@ -96,8 +76,7 @@ run_test() {
tests_run=$((tests_run + 1))
printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}"
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
if (cd "${BASEDIR}/test/tests" && DOTBOT_TEST=true bash "${1}"); then
pass
else
fail

View File

@ -1,37 +1,21 @@
#!/usr/bin/env bash
set -e
BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${BASEDIR}"
export BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${BASEDIR}/test"
. "./driver-lib.bash"
date_stamp="$(date --rfc-3339=ns)"
start="$(date +%s)"
check_prereqs || die "prerequisites unsatsfied."
# command line options
while [[ $# > 1 ]]
do
key="${1}"
case $key in
-v|--version)
VERSION="${2}"
shift && shift
;;
*)
# unknown option
break
;;
esac
done
VERSION="${VERSION:-3.6.4}"
check_env
declare -a tests=()
if [ $# -eq 0 ]; then
while read file; do
tests+=("${file}")
done < <(find tests -type f -name '*.bash')
done < <(find tests -type f -name '*.bash' | sort)
else
tests=("$@")
fi

View File

@ -1,6 +1,5 @@
DEBUG=${DEBUG:-false}
USE_VAGRANT=${USE_VAGRANT:-true}
DOTBOT_EXEC=${DOTBOT_EXEC:-"python /dotbot/bin/dotbot"}
DOTBOT_EXEC="${BASEDIR}/bin/dotbot"
DOTFILES="/home/$(whoami)/dotfiles"
INSTALL_CONF='install.conf.yaml'
INSTALL_CONF_JSON='install.conf.json'
@ -29,17 +28,15 @@ test_expect_failure() {
fi
}
check_vm() {
if [ "$(whoami)" != "vagrant" ]; then
>&2 echo "test can't run outside vm!"
check_env() {
if [ "${DOTBOT_TEST}" != "true" ]; then
>&2 echo "test must be run by test driver"
exit 1
fi
}
initialize() {
if ${USE_VAGRANT}; then
check_vm
fi
check_env
echo "${test_description}"
mkdir -p "${DOTFILES}"
cd

View File

@ -1,81 +0,0 @@
#!/usr/bin/env bash
set -e
# For debug only:
# export DEBUG=true
# set -x
# set -v
export BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Prevent execution outside of Travis CI builds
if [[ "${TRAVIS}" != true || "${CI}" != true ]]; then
echo "Error: `basename "$0"` should only be used on Travis"
exit 2
fi
# Travis runs do not rely on Vagrant
export USE_VAGRANT=false
export DOTBOT_EXEC="${BASEDIR}/bin/dotbot"
cd "${BASEDIR}"
. "test/driver-lib.bash"
travis_initialize() {
echo "initializing."
tests_run=0
tests_passed=0
tests_failed=0
tests_total="${1}"
local plural="" && [ "${tests_total}" -gt 1 ] && plural="s"
printf -- "running %d test%s...\n\n" "${tests_total}" "${plural}"
}
travis_cleanup() {
# Remove all dotfiles installed since the start, ignoring the main
# dotfiles directory, and the dotbot source directory
find ~ -mindepth 1 -newermt "${date_stamp}" \
-not \( -path ~ -o -path "${BASEDIR}/*" \
-o -path ~/dotfiles \) \
-exec rm -rf {} +
}
travis_run_test() {
tests_run=$((tests_run + 1))
printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}"
cd ${BASEDIR}/test/tests
if bash ${1} ; then
pass
else
fail
fi
travis_cleanup || die "unable to clean up system."
}
date_stamp="$(date --rfc-3339=ns)"
start="$(date +%s)"
declare -a tests=()
if [ $# -eq 0 ]; then
while read file; do
tests+=("${file}")
done < <(find ${BASEDIR}/test/tests -type f -name '*.bash')
else
tests=("$@")
fi
travis_initialize "${#tests[@]}"
for file in "${tests[@]}"; do
travis_run_test "$(basename "${file}")"
done
if report; then
ret=0
else
ret=1
fi
echo "(tests run in $(($(date +%s) - start)) seconds)"
exit ${ret}

View File

@ -0,0 +1,19 @@
test_description='clean uses default unless overridden'
. '../test-lib.bash'
test_expect_success 'setup' '
ln -s /nowhere ~/.g
'
test_expect_success 'run' '
run_dotbot <<EOF
- clean:
~/nonexistent:
force: true
~/:
EOF
'
test_expect_success 'test' '
test -h ~/.g
'

View File

@ -0,0 +1,34 @@
test_description='clean removes recursively'
. '../test-lib.bash'
test_expect_success 'setup' '
mkdir -p ~/a/b
ln -s /nowhere ~/c
ln -s /nowhere ~/a/d
ln -s /nowhere ~/a/b/e
'
test_expect_success 'run' '
run_dotbot <<EOF
- clean:
~/:
force: true
EOF
'
test_expect_success 'test' '
! test -h ~/c && test -h ~/a/d && test -h ~/a/b/e
'
test_expect_success 'run 2' '
run_dotbot <<EOF
- clean:
~/:
force: true
recursive: true
EOF
'
test_expect_success 'test 2' '
! test -h ~/a/d && ! test -h ~/a/b/e
'

View File

@ -1,18 +1,16 @@
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 &&
(
IFS=:
for p in $PATH; do
find $p -maxdepth 1 -mindepth 1 -exec sh -c \
if [ -d $p ]; then
find $p -maxdepth 1 -mindepth 1 -exec sh -c \
'"'"'ln -sf {} $HOME/tmp_bin/$(basename {})'"'"' \;
fi
done
) &&
rm -f ~/tmp_bin/python &&

View File

@ -0,0 +1,20 @@
test_description='linking canonicalizes path by default'
. '../test-lib.bash'
test_expect_success 'setup' '
echo "apple" > ${DOTFILES}/f &&
ln -s dotfiles dotfiles-symlink
'
test_expect_success 'run' '
cat > "${DOTFILES}/${INSTALL_CONF}" <<EOF
- link:
~/.f:
path: f
EOF
${DOTBOT_EXEC} -c dotfiles-symlink/${INSTALL_CONF}
'
test_expect_success 'test' '
[ "$(readlink ~/.f | cut -d/ -f4-)" = "dotfiles/f" ]
'

View File

@ -0,0 +1,23 @@
test_description='link is created even if source is missing'
. '../test-lib.bash'
test_expect_failure 'run' '
run_dotbot <<EOF
- link:
~/missing_link:
path: missing
EOF
'
test_expect_success 'run 2' '
run_dotbot <<EOF
- link:
~/missing_link:
path: missing
ignore-missing: true
EOF
'
test_expect_success 'test' '
test -L ~/missing_link
'

View File

@ -0,0 +1,23 @@
test_description='linking path canonicalization can be disabled'
. '../test-lib.bash'
test_expect_success 'setup' '
echo "apple" > ${DOTFILES}/f &&
ln -s dotfiles dotfiles-symlink
'
test_expect_success 'run' '
cat > "${DOTFILES}/${INSTALL_CONF}" <<EOF
- defaults:
link:
canonicalize-path: false
- link:
~/.f:
path: f
EOF
${DOTBOT_EXEC} -c ./dotfiles-symlink/${INSTALL_CONF}
'
test_expect_success 'test' '
[ "$(readlink ~/.f | cut -d/ -f4-)" = "dotfiles-symlink/f" ]
'

View File

@ -4,11 +4,7 @@ test_description='install shim works'
test_expect_success 'setup' '
cd ${DOTFILES}
git init
if ${USE_VAGRANT}; then
git submodule add /dotbot dotbot
else
git submodule add ${BASEDIR} dotbot
fi
git submodule add ${BASEDIR} dotbot
cp ./dotbot/tools/git-submodule/install .
echo "pear" > ${DOTFILES}/foo
'
@ -18,9 +14,6 @@ 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
'