From faf9944a0f7cc4d4f0e62f463b2769a53a544781 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 22 Sep 2023 13:52:14 +0200 Subject: [PATCH] remove un-managed for #403 --- docs/usage.md | 1 + dotdrop/dotdrop.py | 3 +- dotdrop/installer.py | 87 +++++++++++++++++++++++++++------- dotdrop/options.py | 28 ++++++----- tests-ng/install-and-remove.sh | 80 +++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 32 deletions(-) create mode 100755 tests-ng/install-and-remove.sh diff --git a/docs/usage.md b/docs/usage.md index 75c6d42..34a6f4c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -97,6 +97,7 @@ Some available options: * `-a`/`--force-actions`: Force the execution of actions even if the dotfiles are not installed (see [Fake dotfile and actions](config/config-actions.md#fake-dotfile-and-actions) as an alternative) * `-f`/`--force`: Do not ask for any confirmation * `-W`/`--workdir-clear`: Clear the `workdir` before installing dotfiles (see [the config entry](config/config-config.md) `clear_workdir`) +* `-R`/`remove-existing`: Applies to directory dotfiles only (`nolink`) and will remove files not managed by dotdrop in the destination directory To ignore specific patterns during installation, see [the ignore patterns](config/config-file.md#ignore-patterns). diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 932ab75..003c1ac 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -720,7 +720,8 @@ def _get_install_installer(opts, tmpdir=None): totemp=tmpdir, showdiff=opts.install_showdiff, backup_suffix=opts.install_backup_suffix, - diff_cmd=opts.diff_command) + diff_cmd=opts.diff_command, + remove_existing_in_dir=opts.install_remove_existing) return inst diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 775eb70..df1faec 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -28,7 +28,8 @@ class Installer: def __init__(self, base='.', create=True, backup=True, dry=False, safe=False, workdir='~/.config/dotdrop', debug=False, diff=True, totemp=None, showdiff=False, - backup_suffix='.dotdropbak', diff_cmd=''): + backup_suffix='.dotdropbak', diff_cmd='', + remove_existing_in_dir=False): """ @base: directory path where to search for templates @create: create directory hierarchy if missing when installing @@ -42,6 +43,8 @@ class Installer: @showdiff: show the diff before overwriting (or asking for) @backup_suffix: suffix for dotfile backup file @diff_cmd: diff command to use + @remove_existing_in_dir: remove file in dir dotfiles + if not managed by dotdrop """ self.create = create self.backup = backup @@ -60,6 +63,7 @@ class Installer: self.backup_suffix = backup_suffix self.diff_cmd = diff_cmd self.action_executed = False + self.remove_existing_in_dir = remove_existing_in_dir # avoids printing file copied logs # when using install_to_tmp for comparing self.comparing = False @@ -130,10 +134,12 @@ class Installer: if linktype == LinkTypes.NOLINK: # normal file if isdir: - ret, err = self._copy_dir(templater, src, dst, - actionexec=actionexec, - noempty=noempty, ignore=ignore, - is_template=is_template) + ret, err, ins = self._copy_dir(templater, src, dst, + actionexec=actionexec, + noempty=noempty, ignore=ignore, + is_template=is_template) + if self.remove_existing_in_dir and ins: + self._remove_existing_in_dir(dst, ins) else: ret, err = self._copy_file(templater, src, dst, actionexec=actionexec, @@ -583,10 +589,15 @@ class Installer: - False, error_msg : error - False, None : ignored - False, 'aborted' : user aborted + + third arg returned is the list managed dotfiles + in the destination or an empty list if anything + fails """ self.log.dbg(f'deploy dir {src}') # default to nothing installed and no error - ret = False, None + ret = False + dst_dotfiles = [] # handle all files in dir for entry in os.listdir(src): @@ -594,35 +605,75 @@ class Installer: self.log.dbg(f'deploy sub from {dst}: {entry}') if not os.path.isdir(fpath): # is file + fdst = os.path.join(dst, entry) + dst_dotfiles.append(fdst) res, err = self._copy_file(templater, fpath, - os.path.join(dst, entry), + fdst, actionexec=actionexec, noempty=noempty, ignore=ignore, is_template=is_template) if not res and err: # error occured - return res, err + return res, err, [] if res: # something got installed - ret = True, None + + ret = True else: # is directory - res, err = self._copy_dir(templater, fpath, - os.path.join(dst, entry), - actionexec=actionexec, - noempty=noempty, - ignore=ignore, - is_template=is_template) + dpath = os.path.join(dst, entry) + dst_dotfiles.append(dpath) + res, err, subs = self._copy_dir(templater, fpath, + dpath, + actionexec=actionexec, + noempty=noempty, + ignore=ignore, + is_template=is_template) + dst_dotfiles.extend(subs) if not res and err: # error occured - return res, err + return res, err, [] if res: # something got installed - ret = True, None - return ret + ret = True + + return ret, None, dst_dotfiles + + def _is_path_in(self, path, paths): + """return true if path is in paths""" + return any(samefile(path, p) for p in paths) + + def _remove_existing_in_dir(self, directory, installed_files=None): + """ + with --remove-existing this will remove + any file in managed directory which + are not handled by dotdrop + """ + if not installed_files: + return + if not os.path.exists(directory) or not os.path.isdir(directory): + return + to_remove = [] + for root, dirs, files in os.walk(directory): + for name in files: + path = os.path.join(root, name) + if self._is_path_in(path, installed_files): + continue + to_remove.append(os.path.abspath(path)) + for name in dirs: + path = os.path.join(root, name) + if self._is_path_in(path, installed_files): + continue + to_remove.append(os.path.abspath(path)) + for path in to_remove: + if self.dry: + self.log.dry(f'would remove {path}') + continue + self.log.dbg(f'removing not managed: {path}') + removepath(path, logger=self.log) @classmethod def _write_content_to_file(cls, content, src, dst): diff --git a/dotdrop/options.py b/dotdrop/options.py index 2ea8231..3e5b0c3 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -58,19 +58,19 @@ USAGE = f""" {BANNER} Usage: - dotdrop install [-VbtfndDaW] [-c ] [-p ] - [-w ] [...] - dotdrop import [-Vbdfm] [-c ] [-p ] [-i ...] - [--transr=] [--transw=] - [-l ] [-s ] ... - dotdrop compare [-LVbz] [-c ] [-p ] - [-w ] [-C ...] [-i ...] - dotdrop update [-VbfdkPz] [-c ] [-p ] - [-w ] [-i ...] [...] - dotdrop remove [-Vbfdk] [-c ] [-p ] [...] - dotdrop files [-VbTG] [-c ] [-p ] - dotdrop detail [-Vb] [-c ] [-p ] [...] - dotdrop profiles [-VbG] [-c ] + dotdrop install [-VbtfndDaWR] [-c ] [-p ] + [-w ] [...] + dotdrop import [-Vbdfm] [-c ] [-p ] [-i ...] + [--transr=] [--transw=] + [-l ] [-s ] ... + dotdrop compare [-LVbz] [-c ] [-p ] + [-w ] [-C ...] [-i ...] + dotdrop update [-VbfdkPz] [-c ] [-p ] + [-w ] [-i ...] [...] + dotdrop remove [-Vbfdk] [-c ] [-p ] [...] + dotdrop files [-VbTG] [-c ] [-p ] + dotdrop detail [-Vb] [-c ] [-p ] [...] + dotdrop profiles [-VbG] [-c ] dotdrop --help dotdrop --version @@ -91,6 +91,7 @@ Options: -n --nodiff Do not diff when installing. -p --profile= Specify the profile to use [default: {PROFILE}]. -P --show-patch Provide a one-liner to manually patch template. + -R --remove-existing Remove existing file on install directory. -s --as= Import as a different path from actual path. --transr= Associate trans_read key on import. --transw= Apply trans_write key on import. @@ -297,6 +298,7 @@ class Options(AttrMonitor): self.install_force_chmod = self.force_chmod self.install_clear_workdir = self.args['--workdir-clear'] or \ self.clear_workdir + self.install_remove_existing = self.args['--remove-existing'] def _apply_args_compare(self): """compare specifics""" diff --git a/tests-ng/install-and-remove.sh b/tests-ng/install-and-remove.sh new file mode 100755 index 0000000..8d3243d --- /dev/null +++ b/tests-ng/install-and-remove.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2023, deadc0de6 +# +# test install and remove existing file in fs +# returns 1 in case of error +# + +## start-cookie +set -euo errtrace 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 +################################################################ + +# dotdrop directory +basedir=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d) +mkdir -p "${basedir}"/dotfiles +echo "[+] dotdrop dir: ${basedir}" +echo "[+] dotpath dir: ${basedir}/dotfiles" +tmpd=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d) + +clear_on_exit "${basedir}" +clear_on_exit "${tmpd}" + +# create the config file +cfg="${basedir}/config.yaml" +cat > "${cfg}" << _EOF +config: + backup: true + create: true + dotpath: dotfiles +dotfiles: + d_dir: + src: dir + dst: ${tmpd}/dir +profiles: + p1: + dotfiles: + - d_dir +_EOF + +# create the file in dotpath +mkdir -p "${basedir}"/dotfiles/dir +echo "content" > "${basedir}"/dotfiles/dir/file + +# create the file in fs +mkdir -p "${tmpd}"/dir +echo "content" > "${tmpd}"/dir/existing + +echo "[+] install" +cd "${ddpath}" | ${bin} install -c "${cfg}" -f -p p1 --verbose +[ "$?" != "0" ] && exit 1 + +[ ! -e "${tmpd}"/dir/file ] && echo "d_dir file not installed" && exit 1 +[ ! -e "${tmpd}"/dir/existing ] && echo "existing removed" && exit 1 + +echo "[+] install with remove" +cd "${ddpath}" | ${bin} install --remove-existing -c "${cfg}" -f -p p1 --verbose +[ "$?" != "0" ] && exit 1 + +[ ! -e "${tmpd}"/dir/file ] && echo "d_dir file not installed" && exit 1 +[ -e "${tmpd}"/dir/existing ] && echo "existing not removed" && exit 1 + +echo "OK" +exit 0