From a8380f6496d091a13769bd12bb6ca7a89d3618eb Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Sat, 28 Dec 2019 10:44:24 -0500 Subject: [PATCH 01/12] Migrate to travis-ci.com --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a7ff9d..cff7a3b 100644 --- a/README.md +++ b/README.md @@ -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! From eabd84bce1139a5c48089c97cd74bb919a8df331 Mon Sep 17 00:00:00 2001 From: Albert Puig Date: Fri, 25 May 2018 22:04:04 +0200 Subject: [PATCH 02/12] Add ignore-missing option to link --- README.md | 1 + dotbot/plugins/link.py | 18 ++++++++++++------ test/tests/link-ignore-missing.bash | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 test/tests/link-ignore-missing.bash diff --git a/README.md b/README.md index cff7a3b..00094aa 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ Available extended configuration parameters: | `relative` | Use a relative path to the source 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. | +| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default:false) | #### Example diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index 0b47ac6..bf3db3e 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -31,6 +31,7 @@ class Link(dotbot.Plugin): 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) @@ -39,6 +40,7 @@ class Link(dotbot.Plugin): 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) @@ -67,7 +69,7 @@ class Link(dotbot.Plugin): success &= self._create(destination) if force or relink: success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative) + success &= self._link(path, destination, relative, ignore_missing) else: self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) glob_base = path[:glob_star_loc] @@ -78,18 +80,22 @@ class Link(dotbot.Plugin): 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._link(glob_full_item, glob_link_destination, relative, 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._link(path, destination, relative, ignore_missing) if success: self._log.info('All links have been set up') else: @@ -189,7 +195,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, ignore_missing): ''' Links link_name to source. @@ -209,7 +215,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: diff --git a/test/tests/link-ignore-missing.bash b/test/tests/link-ignore-missing.bash new file mode 100644 index 0000000..4978279 --- /dev/null +++ b/test/tests/link-ignore-missing.bash @@ -0,0 +1,23 @@ +test_description='link is created even if source is missing' +. '../test-lib.bash' + +test_expect_failure 'run' ' +run_dotbot < Date: Tue, 31 Dec 2019 14:47:32 -0500 Subject: [PATCH 03/12] Fix clean not respecting defaults Previously, clean read the defaults once, and then it updated the setting for each entry it read. This resulted in the defaults being clobbered and then not being respected for subsequent entries. This patch fixes the issue by re-reading the defaults before processing each item. The other plugins (link, shell) do not have this problem. --- dotbot/plugins/clean.py | 4 ++-- test/tests/clean-default.bash | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 test/tests/clean-default.bash diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py index 22c975e..89251b9 100644 --- a/dotbot/plugins/clean.py +++ b/dotbot/plugins/clean.py @@ -18,9 +18,9 @@ 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) + if isinstance(targets, dict) and isinstance(targets[target], dict): force = targets[target].get('force', force) success &= self._clean(target, force) if success: diff --git a/test/tests/clean-default.bash b/test/tests/clean-default.bash new file mode 100644 index 0000000..8bb405d --- /dev/null +++ b/test/tests/clean-default.bash @@ -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 < Date: Tue, 31 Dec 2019 19:14:23 -0500 Subject: [PATCH 04/12] Add option to clean recursively --- README.md | 8 ++++++-- dotbot/plugins/clean.py | 11 +++++++++-- test/tests/clean-recursive.bash | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 test/tests/clean-recursive.bash diff --git a/README.md b/README.md index 00094aa..ca7242d 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,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 @@ -311,8 +313,10 @@ point to a file inside the dotfiles directory. - clean: ['~'] - clean: - ~/.config: + ~/: force: true + ~/.config: + recursive: true ``` ### Defaults diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py index 89251b9..82d77a6 100644 --- a/dotbot/plugins/clean.py +++ b/dotbot/plugins/clean.py @@ -20,16 +20,18 @@ class Clean(dotbot.Plugin): defaults = self._context.defaults().get(self._directive, {}) for target in targets: 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: diff --git a/test/tests/clean-recursive.bash b/test/tests/clean-recursive.bash new file mode 100644 index 0000000..8d8c09d --- /dev/null +++ b/test/tests/clean-recursive.bash @@ -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 < Date: Fri, 3 Jan 2020 14:00:13 -0500 Subject: [PATCH 05/12] Add Python 3.8 to Travis tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2e19a14..bece3e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" - "nightly" - "pypy3" From 1e1885c45a28190dc1cbde993a9ddcf1729ee4d1 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Fri, 3 Jan 2020 14:42:45 -0500 Subject: [PATCH 06/12] Fix incorrect use of `is` over `==` Comparing strings and integers with `is` is a bug: comparisons should be done with `==`. It might not have caused observable problems in the past because small integers and strings can be interned. --- dotbot/plugins/link.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index bf3db3e..d38c0ab 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -51,19 +51,19 @@ 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) From 6d24613b0b453e546fa4e9defc695d8f982743c9 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Fri, 3 Jan 2020 15:20:00 -0500 Subject: [PATCH 07/12] Unify Vagrant and Travis-CI tests This patch makes the tests (including the test driver) run entirely inside Vagrant, which avoids calling the very slow `vagrant` driver many times for running the tests. On my machine, `./test` runs in 22 seconds, down from hundreds of seconds prior to this patch. This also has the nice side effect of matching how the Travis CI tests were run, so there's no need for a separate `test_travis` anymore. --- .travis.yml | 2 +- test/README.md | 22 +++++-- test/Vagrantfile | 4 +- test/driver-lib.bash | 53 +++++------------ test/test | 26 ++------- test/test-lib.bash | 13 ++--- test/test_travis | 81 -------------------------- test/tests/find-python-executable.bash | 8 +-- test/tests/shim.bash | 9 +-- 9 files changed, 49 insertions(+), 169 deletions(-) delete mode 100755 test/test_travis diff --git a/.travis.yml b/.travis.yml index bece3e9..f81b161 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,4 +13,4 @@ python: sudo: false script: - - ./test/test_travis + - ./test/test diff --git a/test/README.md b/test/README.md index 993a753..9ec79f5 100644 --- a/test/README.md +++ b/test/README.md @@ -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 -` - 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`. diff --git a/test/Vagrantfile b/test/Vagrantfile index 347c402..fbdfe39 100644 --- a/test/Vagrantfile +++ b/test/Vagrantfile @@ -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 diff --git a/test/driver-lib.bash b/test/driver-lib.bash index 02a71a5..dcbcba6 100644 --- a/test/driver-lib.bash +++ b/test/driver-lib.bash @@ -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 diff --git a/test/test b/test/test index c018c32..f944512 100755 --- a/test/test +++ b/test/test @@ -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 diff --git a/test/test-lib.bash b/test/test-lib.bash index e4d9a4e..fba9aa0 100644 --- a/test/test-lib.bash +++ b/test/test-lib.bash @@ -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 diff --git a/test/test_travis b/test/test_travis deleted file mode 100755 index 79439e1..0000000 --- a/test/test_travis +++ /dev/null @@ -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} diff --git a/test/tests/find-python-executable.bash b/test/tests/find-python-executable.bash index 9a91a74..561b882 100644 --- a/test/tests/find-python-executable.bash +++ b/test/tests/find-python-executable.bash @@ -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 && diff --git a/test/tests/shim.bash b/test/tests/shim.bash index 2ed7d54..ddacae3 100644 --- a/test/tests/shim.bash +++ b/test/tests/shim.bash @@ -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 < Date: Fri, 3 Jan 2020 16:19:21 -0500 Subject: [PATCH 08/12] Update dates --- LICENSE.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 922999c..89a93af 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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 diff --git a/README.md b/README.md index ca7242d..20c69d2 100644 --- a/README.md +++ b/README.md @@ -378,7 +378,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/ From 138fdbc8d7f42bd54e0a1d0c07d5858b1055ea50 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Mon, 21 May 2018 19:10:17 +0200 Subject: [PATCH 09/12] Add 'canonicalize-path' option to link Dotbot had a hardcoded behaviour that the BASEDIR was always passed to os.path.realpath which "returns the canonical path of the specified filename, eliminating any symbolic links encountered in the path". This might not always be desirable so this commit makes it configurable. The use case where `canonicalize-path` comes in handy is the following: You want to provide dotfiles in the Filesystem Hierarchy Standard under `/usr/local/share/ypid_dotfiles/`. Now you want to provide `.config/dotfiles` as a default in `/etc/skel`. When you now pre-configure `/etc/skel` by running dotbot in it set has HOME, dotfiles will refer to `/usr/local/share/ypid_dotfiles/` and not `/etc/skel/.config/dotfiles` which does not look nice. This is related to but not the same as the `relative` parameter used with link commands. --- README.md | 1 + dotbot/context.py | 8 ++++++-- dotbot/dispatcher.py | 4 ++-- dotbot/plugins/link.py | 23 +++++++++++++---------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ca7242d..6f6db79 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ 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) | diff --git a/dotbot/context.py b/dotbot/context.py index b2dbd6c..8c42d47 100644 --- a/dotbot/context.py +++ b/dotbot/context.py @@ -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 diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index d1a4f95..36eac02 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -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) diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index bf3db3e..9ba5540 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -26,6 +26,7 @@ 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) @@ -36,6 +37,7 @@ class Link(dotbot.Plugin): # 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) @@ -68,8 +70,8 @@ class Link(dotbot.Plugin): if create: success &= self._create(destination) if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative, ignore_missing) + 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] @@ -79,8 +81,8 @@ 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, ignore_missing) + 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) @@ -94,8 +96,8 @@ class Link(dotbot.Plugin): (destination, path)) continue if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative, ignore_missing) + 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: @@ -159,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) @@ -195,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, ignore_missing): + def _link(self, source, link_name, relative, canonical_path, ignore_missing): ''' Links link_name to source. @@ -203,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: From 320d5d0123b045fe922cf576d8d65e2db0517910 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Fri, 3 Jan 2020 16:07:44 -0500 Subject: [PATCH 10/12] Add tests for canonicalize-path --- dotbot/cli.py | 4 ++-- test/tests/link-canonicalize.bash | 20 ++++++++++++++++++++ test/tests/link-no-canonicalize.bash | 23 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/tests/link-canonicalize.bash create mode 100644 test/tests/link-no-canonicalize.bash diff --git a/dotbot/cli.py b/dotbot/cli.py index 2680acf..77bd439 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -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) diff --git a/test/tests/link-canonicalize.bash b/test/tests/link-canonicalize.bash new file mode 100644 index 0000000..34015c8 --- /dev/null +++ b/test/tests/link-canonicalize.bash @@ -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}" < ${DOTFILES}/f && +ln -s dotfiles dotfiles-symlink +' + +test_expect_success 'run' ' +cat > "${DOTFILES}/${INSTALL_CONF}" < Date: Fri, 3 Jan 2020 16:47:57 -0500 Subject: [PATCH 11/12] Release 1.17.0 --- dotbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotbot/__init__.py b/dotbot/__init__.py index e1fafd9..45c4cf9 100644 --- a/dotbot/__init__.py +++ b/dotbot/__init__.py @@ -1,4 +1,4 @@ from .cli import main from .plugin import Plugin -__version__ = '1.16.0' +__version__ = '1.17.0' From 5d83f9e797b1950199e127a8196803f5e33e0916 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Mon, 6 Jan 2020 20:11:22 -0500 Subject: [PATCH 12/12] Upgrade PyYAML to 5.3 --- lib/pyyaml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pyyaml b/lib/pyyaml index 0f64cbf..2f463cf 160000 --- a/lib/pyyaml +++ b/lib/pyyaml @@ -1 +1 @@ -Subproject commit 0f64cbfa54b0b22dc7b776b7b98a7cd657e84d78 +Subproject commit 2f463cf5b0e98a52bc20e348d1e69761bf263b86 diff --git a/setup.py b/setup.py index 6fe1d14..e3b2198 100644 --- a/setup.py +++ b/setup.py @@ -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