diff --git a/dotdrop/comparator.py b/dotdrop/comparator.py index 25ed154..85a835d 100644 --- a/dotdrop/comparator.py +++ b/dotdrop/comparator.py @@ -6,12 +6,11 @@ handle the comparison of two dotfiles """ import os -import filecmp # local imports from dotdrop.logger import Logger from dotdrop.ftree import FTreeDir -from dotdrop.utils import must_ignore, uniq_list, diff, \ +from dotdrop.utils import must_ignore, diff, \ get_file_perm diff --git a/dotdrop/ftree.py b/dotdrop/ftree.py index d4cc1b7..8b29c33 100644 --- a/dotdrop/ftree.py +++ b/dotdrop/ftree.py @@ -10,6 +10,7 @@ import os # local imports from dotdrop.utils import must_ignore +from dotdrop.logger import Logger class FTreeDir: @@ -22,6 +23,7 @@ class FTreeDir: self.ignores = ignores self.debug = debug self.entries = [] + self.log = Logger(debug=self.debug) if os.path.exists(path) and os.path.isdir(path): self._walk() @@ -37,10 +39,12 @@ class FTreeDir: if must_ignore([fpath], ignores=self.ignores, debug=self.debug, strict=True): continue + self.log.dbg(f'added file to list of {self.path}: {fpath}') self.entries.append(fpath) for dname in dirs: dpath = os.path.join(root, dname) - if len(os.listdir(dpath)) < 1: + subs = os.listdir(dpath) + if len(subs) < 1: # ignore empty directory continue # appending "/" allows to ensure pattern @@ -50,6 +54,7 @@ class FTreeDir: if must_ignore([dpath], ignores=self.ignores, debug=self.debug, strict=True): continue + self.log.dbg(f'added dir to list of {self.path}: {dpath}') self.entries.append(dpath) def compare(self, other): diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 2633be1..9997967 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -11,10 +11,12 @@ import filecmp # local imports from dotdrop.logger import Logger +from dotdrop.ftree import FTreeDir from dotdrop.templategen import Templategen from dotdrop.utils import ignores_to_absolute, removepath, \ get_unique_tmp_name, write_to_tmpfile, must_ignore, \ - mirror_file_rights, get_file_perm, copytree_with_ign + mirror_file_rights, get_file_perm, copytree_with_ign, \ + diff from dotdrop.exceptions import UndefinedException @@ -137,6 +139,8 @@ class Updater: else: ret = self._handle_file(new_path, local_path, ignores) + if not ret: + return False # mirror rights if deployed_mode != local_mode: @@ -144,6 +148,7 @@ class Updater: self.log.dbg(msg) if self.conf.update_dotfile(dotfile.key, deployed_mode): ret = True + self._mirror_file_perms(deployed_path, local_path) # clean temporary files if new_path != deployed_path and os.path.exists(new_path): @@ -244,8 +249,8 @@ class Updater: self.log.dry(f'would cp {deployed_path} {local_path}') else: self.log.dbg(f'cp {deployed_path} {local_path}') - shutil.copyfile(deployed_path, local_path) - self._mirror_file_perms(deployed_path, local_path) + shutil.copy2(deployed_path, local_path) + # self._mirror_file_perms(deployed_path, local_path) self.log.sub(f'\"{local_path}\" updated') except IOError as exc: self.log.warn(f'{deployed_path} update failed, do manually: {exc}') @@ -256,6 +261,86 @@ class Updater: dotfile, ignores): """sync path (local dir) and local_path (dotdrop dir path)""" self.log.dbg(f'handle update for dir {deployed_path} to {local_path}') + + # get absolute paths + deployed_path = os.path.expanduser(deployed_path) + local_path = os.path.expanduser(local_path) + + local_tree = FTreeDir(local_path, + ignores=ignores, + debug=self.debug) + deploy_tree = FTreeDir(deployed_path, + ignores=ignores, + debug=self.debug) + lonly, ronly, common = local_tree.compare(deploy_tree) + print(f'lonly: {lonly}') + print(f'ronly: {ronly}') + print(f'common: {common}') + + # those only in dotpath + for i in lonly: + path = os.path.join(local_path, i) + if self.dry: + self.log.dry(f'would rm -r {path}') + continue + self.log.dbg(f'rm -r {path}') + if not self._confirm_rm_r(path): + continue + removepath(path, logger=self.log) + self.log.sub(f'\"{path}\" removed') + + ignore_missing_in_dotdrop = self.ignore_missing_in_dotdrop or \ + dotfile.ignore_missing_in_dotdrop + if not ignore_missing_in_dotdrop: + for i in ronly: + # only in deployed dir + srcpath = os.path.join(deployed_path, i) + dstpath = os.path.join(local_path, i) + if self.dry: + self.log.dry(f'would cp -r {srcpath} {dstpath}') + continue + self.log.dbg(f'cp {srcpath} {dstpath}') + try: + if not os.path.isdir(srcpath): + # we do not care about directory since + # those are handled by shutil automatically + os.makedirs(os.path.dirname(dstpath), exist_ok=True) + shutil.copy2(srcpath, dstpath) + # self._mirror_file_perms(srcpath, dstpath) + except IOError as exc: + msg = f'{srcpath} update failed, do manually: {exc}' + self.log.warn(msg) + return False + self.log.sub(f'\"{dstpath}\" updated') + + for i in common: + srcpath = os.path.join(deployed_path, i) + dstpath = os.path.join(local_path, i) + if os.path.isdir(srcpath): + continue + out = diff(modified=dstpath, original=srcpath, + debug=self.debug) + if not out: + continue + if self.dry: + self.log.dry(f'would update content of {dstpath} from {srcpath}') + continue + self.log.dbg(f'cp {srcpath} {dstpath}') + try: + shutil.copy2(srcpath, dstpath) + self._mirror_file_perms(srcpath, dstpath) + except IOError as exc: + msg = f'{srcpath} update failed, do manually: {exc}' + self.log.warn(msg) + return False + self.log.sub(f'\"{dstpath}\" content updated') + + return True + + def _handle_dir2(self, deployed_path, local_path, + dotfile, ignores): + """sync path (local dir) and local_path (dotdrop dir path)""" + self.log.dbg(f'handle update for dir {deployed_path} to {local_path}') # paths must be absolute (no tildes) deployed_path = os.path.expanduser(deployed_path) local_path = os.path.expanduser(local_path) diff --git a/tests-ng/ignore-dir-when-sub-ignored.sh b/tests-ng/ignore-dir-when-sub-ignored.sh new file mode 100755 index 0000000..193715b --- /dev/null +++ b/tests-ng/ignore-dir-when-sub-ignored.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2024, deadc0de6 +# +# test ignore patterns and especially that if +# the directory content is ignored, so is the directory itself +# returns 1 in case of error +# + +## start-cookie +set -eu -o errtrace -o pipefail +cur=$(cd "$(dirname "${0}")" && pwd) +ddpath="${cur}/../" +PPATH="{PYTHONPATH:-}" +export PYTHONPATH="${ddpath}:${PPATH}" +altbin="python3 -m dotdrop.dotdrop" +if hash coverage 2>/dev/null; then + mkdir -p coverages/ + altbin="coverage run -p --data-file coverages/coverage --source=dotdrop -m dotdrop.dotdrop" +fi +bin="${DT_BIN:-${altbin}}" +# shellcheck source=tests-ng/helpers +source "${cur}"/helpers +echo -e "$(tput setaf 6)==> RUNNING $(basename "${BASH_SOURCE[0]}") <==$(tput sgr0)" +## end-cookie + +################################################################ +# this is the test +################################################################ + +# the dotfile source +tmps=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d) +dotpath="${tmps}"/dotfiles +mkdir -p "${dotpath}" +#echo "dotfile source: ${tmps}" +# the dotfile destination +tmpd=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d) +#echo "dotfile destination: ${tmpd}" + +clear_on_exit "${tmps}" +clear_on_exit "${tmpd}" + +# create the config file +cfg1="${tmps}/config1.yaml" +cfg2="${tmps}/config2.yaml" + +cat > "${cfg1}" << _EOF +config: + backup: true + create: true + dotpath: dotfiles + ignoreempty: true +dotfiles: + d_mpv: + src: mpv + dst: ${tmpd}/mpv + cmpignore: + - '*/watch_later/x' + upignore: + - '*/watch_later/x' + instignore: + - '*/watch_later/x' +profiles: + p1: + dotfiles: + - d_mpv +_EOF + +cat > "${cfg2}" << _EOF +config: + backup: true + create: true + dotpath: dotfiles + ignoreempty: true + impignore: + - '*/watch_later/x' +dotfiles: +profiles: +_EOF + +clean_both() +{ + rm -rf "${dotpath}/mpv" + rm -rf "${tmpd}/mpv" +} + +# $1 parent +create_hierarchy() +{ + mkdir -p "${1}"/mpv + echo "file" > "${1}"/mpv/file + mkdir -p "${1}"/mpv/dir1 + echo "file2" > "${1}"/mpv/dir1/file + mkdir -p "${1}"/mpv/watch_later + echo "watch_later" > "${1}"/mpv/watch_later/x +} + +create_in_dotpath() +{ + create_hierarchy "${dotpath}" +} + +create_in_dst() +{ + create_hierarchy "${tmpd}" +} + +################################################### +# test install +################################################### +clean_both +create_in_dotpath +cd "${ddpath}" | ${bin} install -f -c "${cfg1}" -p p1 -V +[ -d "${tmpd}/mpv/watch_later" ] && echo "install failed" && exit 1 + +################################################### +# test update +################################################### +clean_both +create_in_dotpath +create_in_dst + +# modify +echo newfile > "${tmpd}/mpv/new" +rm -rf "${dotpath}/mpv/watch_later" + +cd "${ddpath}" | ${bin} update -f -c "${cfg1}" -p p1 -V +[ -d "${dotpath}/mpv/watch_later" ] && echo "update failed - watch_later created" && exit 1 +[ -e "${dotpath}/mpv/watch_later/x" ] && echo "update failed - x added" && exit 1 +[ ! -e "${dotpath}/mpv/new" ] && echo "update failed - no new file" && exit 1 + +################################################### +# test import +################################################### +exit 0 # TODO +clean_both +create_in_dst + +cd "${ddpath}" | ${bin} import -f -c "${cfg2}" -p p1 -V "${tmpd}/mpv" +[ -d "${dotpath}/${tmpd}/mpv/watch_later" ] && echo "import failed" && exit 1 +[ ! -e "${dotpath}/${tmpd}/mpv/file" ] && echo "import failed - file" && exit 1 + +################################################### +# test compare +################################################### +clean_both +create_in_dst +create_in_dotpath + +rm -r "${dotpath}/mpv/watch_later" +cd "${ddpath}" | ${bin} compare -c "${cfg1}" -p p1 -V + +################################################### + +echo "OK" +exit 0 diff --git a/tests-ng/update-ignore.sh b/tests-ng/update-ignore.sh index ea1e28e..c1c7ed6 100755 --- a/tests-ng/update-ignore.sh +++ b/tests-ng/update-ignore.sh @@ -74,7 +74,7 @@ echo "c" > "${tmpd}"/a/x/yfile # update "dir" filesystem echo "new" > "${tmpd}"/dir/a/a -mkdir -p "${dt}"/dir/a/be-gone +touch "${dt}"/dir/a/be-gone touch "${tmpd}"/dir/newfile mkdir -p "${tmpd}"/dir/ignore echo "ignore-me" > "${tmpd}"/dir/ignore/ignore-me @@ -117,12 +117,12 @@ cd "${ddpath}" | ${bin} update -f --verbose -c "${cfg}" --profile=p1 grep_or_fail 'b' "${dt}/a/c/acfile" grep_or_fail 'a' "${dt}/a/x/xfile" [ -e "${dt}"/a/newfile ] && echo "'a' newfile should have been removed" && exit 1 -[ -d "${dt}"/a/be-gone ] && echo "'a' be-gone should have been removed" && exit 1 +[ -e "${dt}"/a/be-gone ] && echo "'file' be-gone should have been removed" && exit 1 [ -e "${dt}"/x/yfile ] && echo "'a' yfile should not have been added" && exit 1 # check "dir" files are correct grep_or_fail 'new' "${dt}"/dir/a/a -[ -d "${dt}"/dir/a/be-gone ] && echo "'dir' be-gone should have been removed" && exit 1 +[ -e "${dt}"/dir/a/be-gone ] && echo "'file' be-gone should have been removed" && exit 1 [ ! -e "${tmpd}"/dir/newfile ] && echo "'dir' newfile should have been removed" && exit 1 [ -d "${dt}"/dir/ignore ] && echo "'dir' ignore dir not ignored" && exit 1 [ -f "${dt}"/dir/ignore/ignore-me ] && echo "'dir' ignore-me not ignored" && exit 1