diff --git a/docs/config-format.md b/docs/config-format.md index 36ea314..d9d2b56 100644 --- a/docs/config-format.md +++ b/docs/config-format.md @@ -16,6 +16,7 @@ Entry | Description | Default `check_version` | Check if a new version of dotdrop is available on github | false `chmod_on_import` | Always add a chmod entry on newly imported dotfiles (see `--preserve-mode`) | false `clear_workdir` | On `install` clear the `workdir` before installing dotfiles (see `--workdir-clear`) | false +`compare_workdir` | On `compare` notify on files in `workdir` not tracked by dotdrop | false `cmpignore` | List of patterns to ignore when comparing, applied to all dotfiles (enclose in quotes when using wildcards; see [ignore patterns](config.md#ignore-patterns)) | - `create` | Create a directory hierarchy when installing dotfiles if it doesn't exist | true `default_actions` | List of action keys to execute for all installed dotfiles (See [actions](config-details.md#actions-entry)) | - diff --git a/docs/usage.md b/docs/usage.md index 298a810..fef175b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -112,6 +112,9 @@ To ignore specific patterns, see [the ignore patterns](config.md#ignore-patterns To completely ignore all files not present in `dotpath` see [Ignore missing](#ignore-missing). +If you want to get notified on files present in the `workdir` but not tracked +by dotdrop see the [compare_workdir](config-format.md). + For more options, see the usage with `dotdrop --help`. ## List profiles diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index d41a16a..9fdfab1 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -8,6 +8,7 @@ entry point import os import sys import time +import fnmatch from concurrent import futures # local imports @@ -20,7 +21,7 @@ from dotdrop.comparator import Comparator from dotdrop.importer import Importer from dotdrop.utils import get_tmpdir, removepath, \ uniq_list, patch_ignores, dependencies_met, \ - adapt_workers, check_version + adapt_workers, check_version, pivot_path from dotdrop.linktypes import LinkTypes from dotdrop.exceptions import YamlException, \ UndefinedException, UnmetDependency @@ -379,6 +380,44 @@ def cmd_install(opts): return True +def _workdir_enum(opts): + workdir_files = [] + for root, _, files in os.walk(opts.workdir): + for file in files: + fpath = os.path.join(root, file) + workdir_files.append(fpath) + + for dotfile in opts.dotfiles: + src = os.path.join(opts.dotpath, dotfile.src) + if dotfile.link == LinkTypes.NOLINK: + # ignore not link files + continue + if not Templategen.is_template(src): + # ignore not template + continue + newpath = pivot_path(dotfile.dst, opts.workdir, + striphome=True, logger=None) + if os.path.isdir(newpath): + # recursive + pattern = '{}/*'.format(newpath) + files = workdir_files.copy() + for file in files: + if fnmatch.fnmatch(file, pattern): + workdir_files.remove(file) + # only checks children + children = [f.path for f in os.scandir(newpath)] + for child in children: + if child in workdir_files: + workdir_files.remove(child) + else: + if newpath in workdir_files: + workdir_files.remove(newpath) + for wfile in workdir_files: + line = '=> \"{}\" does not exist in dotdrop' + LOG.log(line.format(wfile)) + return len(workdir_files) + + def cmd_compare(opts, tmp): """compare dotfiles and return True if all identical""" dotfiles = opts.dotfiles @@ -424,6 +463,9 @@ def cmd_compare(opts, tmp): same = False cnt += 1 + if opts.compare_workdir and _workdir_enum(opts) > 0: + same = False + LOG.log('\n{} dotfile(s) compared.'.format(cnt)) return same diff --git a/dotdrop/installer.py b/dotdrop/installer.py index fd2a69a..db05773 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -224,7 +224,7 @@ class Installer: self.totemp = None # install the dotfile to a temp directory - tmpdst = self._pivot_path(dst, tmpdir) + tmpdst = utils.pivot_path(dst, tmpdir, logger=self.log) ret, err = self.install(templater, src, tmpdst, LinkTypes.NOLINK, is_template=is_template, @@ -260,7 +260,8 @@ class Installer: if is_template: self.log.dbg('is a template') self.log.dbg('install to {}'.format(self.workdir)) - tmp = self._pivot_path(dst, self.workdir, striphome=True) + tmp = utils.pivot_path(dst, self.workdir, + striphome=True, logger=self.log) ret, err = self.install(templater, src, tmp, LinkTypes.NOLINK, actionexec=actionexec, @@ -326,7 +327,8 @@ class Installer: self.log.dbg('child is a template') self.log.dbg('install to {} and symlink' .format(self.workdir)) - tmp = self._pivot_path(subdst, self.workdir, striphome=True) + tmp = utils.pivot_path(subdst, self.workdir, + striphome=True, logger=self.log) ret2, err2 = self.install(templater, subsrc, tmp, LinkTypes.NOLINK, actionexec=actionexec, @@ -698,17 +700,6 @@ class Installer: self.log.log('backup {} to {}'.format(path, dst)) os.rename(path, dst) - def _pivot_path(self, path, newdir, striphome=False): - """change path to be under newdir""" - self.log.dbg('pivot new dir: \"{}\"'.format(newdir)) - self.log.dbg('strip home: {}'.format(striphome)) - if striphome: - path = utils.strip_home(path) - sub = path.lstrip(os.sep) - new = os.path.join(newdir, sub) - self.log.dbg('pivot \"{}\" to \"{}\"'.format(path, new)) - return new - def _exec_pre_actions(self, actionexec): """execute action executor""" if self.action_executed: diff --git a/dotdrop/settings.py b/dotdrop/settings.py index 54554d2..eb36cbd 100644 --- a/dotdrop/settings.py +++ b/dotdrop/settings.py @@ -47,6 +47,7 @@ class Settings(DictParser): key_chmod_on_import = 'chmod_on_import' key_check_version = 'check_version' key_clear_workdir = 'clear_workdir' + key_compare_workdir = 'compare_workdir' # import keys key_import_actions = 'import_actions' @@ -67,7 +68,8 @@ class Settings(DictParser): template_dotfile_default=True, ignore_missing_in_dotdrop=False, force_chmod=False, chmod_on_import=False, - check_version=False, clear_workdir=False): + check_version=False, clear_workdir=False, + compare_workdir=False): self.backup = backup self.banner = banner self.create = create @@ -99,6 +101,7 @@ class Settings(DictParser): self.chmod_on_import = chmod_on_import self.check_version = check_version self.clear_workdir = clear_workdir + self.compare_workdir = compare_workdir def _serialize_seq(self, name, dic): """serialize attribute 'name' into 'dic'""" @@ -127,6 +130,7 @@ class Settings(DictParser): self.key_chmod_on_import: self.chmod_on_import, self.key_check_version: self.check_version, self.key_clear_workdir: self.clear_workdir, + self.key_compare_workdir: self.compare_workdir, } self._serialize_seq(self.key_default_actions, dic) self._serialize_seq(self.key_import_actions, dic) diff --git a/dotdrop/utils.py b/dotdrop/utils.py index 67798ae..38261f6 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -478,3 +478,17 @@ def check_version(): if version.parse(VERSION) < version.parse(latest): msg = 'A new version of dotdrop is available ({})' LOG.warn(msg.format(latest)) + + +def pivot_path(path, newdir, striphome=False, logger=None): + """change path to be under newdir""" + if logger: + logger.dbg('pivot new dir: \"{}\"'.format(newdir)) + logger.dbg('strip home: {}'.format(striphome)) + if striphome: + path = strip_home(path) + sub = path.lstrip(os.sep) + new = os.path.join(newdir, sub) + if logger: + logger.dbg('pivot \"{}\" to \"{}\"'.format(path, new)) + return new diff --git a/tests-ng/workdir-compare.sh b/tests-ng/workdir-compare.sh new file mode 100755 index 0000000..e7371de --- /dev/null +++ b/tests-ng/workdir-compare.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2021, deadc0de6 +# +# test workdir compare and warn on untracked files +# returns 1 in case of error +# + +# exit on first error +set -e + +# all this crap to get current path +rl="readlink -f" +if ! ${rl} "${0}" >/dev/null 2>&1; then + rl="realpath" + + if ! hash ${rl}; then + echo "\"${rl}\" not found !" && exit 1 + fi +fi +cur=$(dirname "$(${rl} "${0}")") + +#hash dotdrop >/dev/null 2>&1 +#[ "$?" != "0" ] && echo "install dotdrop to run tests" && exit 1 + +#echo "called with ${1}" + +# dotdrop path can be pass as argument +ddpath="${cur}/../" +[ "${1}" != "" ] && ddpath="${1}" +[ ! -d ${ddpath} ] && echo "ddpath \"${ddpath}\" is not a directory" && exit 1 + +export PYTHONPATH="${ddpath}:${PYTHONPATH}" +bin="python3 -m dotdrop.dotdrop" +hash coverage 2>/dev/null && bin="coverage run -a --source=dotdrop -m dotdrop.dotdrop" || true + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "$(tput setaf 6)==> RUNNING $(basename $BASH_SOURCE) <==$(tput sgr0)" + +################################################################ +# this is the test +################################################################ +unset DOTDROP_WORKDIR +string="blabla" + +# the dotfile source +tmp=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` + +tmpf="${tmp}/dotfiles" +tmpw="${tmp}/workdir" + +mkdir -p ${tmpf} +echo "dotfiles source (dotpath): ${tmpf}" +mkdir -p ${tmpw} +echo "workdir: ${tmpw}" + +# create the config file +cfg="${tmp}/config.yaml" +echo "config file: ${cfg}" + +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +echo "dotfiles destination: ${tmpd}" + +clear_on_exit "${tmp}" +clear_on_exit "${tmpd}" + +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles + workdir: ${tmpw} + compare_workdir: true +dotfiles: + f_a: + dst: ${tmpd}/a + src: a + link: link + f_b: + dst: ${tmpd}/b + src: b + link: nolink + d_c: + dst: ${tmpd}/c + src: c + link: link_children +profiles: + p1: + dotfiles: + - f_a + - f_b + - d_c +_EOF +#cat ${cfg} + +# create the dotfile +echo "{{@@ profile @@}}" > ${tmpf}/a +echo "{{@@ profile @@}}" > ${tmpf}/b +mkdir -p ${tmpf}/c +echo "{{@@ profile @@}}" > ${tmpf}/c/a +echo "{{@@ profile @@}}" > ${tmpf}/c/b +mkdir ${tmpf}/c/x +echo "{{@@ profile @@}}" > ${tmpf}/c/x/a +echo "{{@@ profile @@}}" > ${tmpf}/c/x/b + +# install +cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -b + +# compare (no diff) +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 -b -V + +# add file +touch ${tmpw}/untrack + +# compare (one diff) +set +e +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 -b -V +[ "$?" != "1" ] && echo "not found untracked file in workdir (1)" && exit 1 +set -e + +# clean +rm ${tmpw}/untrack +# add sub file +touch ${tmpw}/${tmpd}/c/x/untrack + +# compare (two diff) +set +e +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 -b -V +[ "$?" != "1" ] && echo "not found untracked file in workdir (2)" && exit 1 +set -e + +# clean +rm ${tmpw}/${tmpd}/c/x/untrack +# add dir +mkdir ${tmpw}/d_untrack +touch ${tmpw}/d_untrack/untrack + +# compare (three diffs) +set +e +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 -b -V +[ "$?" != "1" ] && echo "not found untracked file in workdir (3)" && exit 1 +set -e + +# clean +rm -r ${tmpw}/d_untrack +# add sub dir +mkdir ${tmpw}/${tmpd}/c/x/d_untrack +touch ${tmpw}/${tmpd}/c/x/d_untrack/untrack + +# compare +set +e +cd ${ddpath} | ${bin} compare -c ${cfg} -p p1 -b -V +[ "$?" != "1" ] && echo "not found untracked file in workdir (4)" && exit 1 +set -e + +## CLEANING +rm -rf ${tmp} ${tmpd} + +echo "OK" +exit 0