diff --git a/README.md b/README.md index 066f1b8..8c440c6 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ that don't need to appear in the output of compare. Either use the command line switch `-i --ignore` or add an entry in the dotfile directly in the `cmpignore` entry (see [Config](#config)). -The pattern follows Unix shell-style wildcards like for example `*/path/file`. +The ignore pattern must follow Unix shell-style wildcards like for example `*/path/file`. Make sure to quote those when using wildcards in the config file. It is also possible to install all dotfiles for a specific profile @@ -474,6 +474,21 @@ $ dotdrop update ~/.vimrc $ dotdrop update --key f_vimrc ``` +It is possible to ignore files to update using unix pattern by providing those +either through the switch `-i --ignore` or as part of the dotfile under the +key `upignore` (see [Config](#config)). +The ignore pattern must follow Unix shell-style wildcards like for example `*/path/file`. +Make sure to quote those when using wildcards in the config file. +```yaml +dotfiles: + d_vim + dst: ~/.vim + src: vim + upignore: + - "*/undo-dir" + - "*/plugged" +``` + There are two cases when updating a dotfile: **The dotfile doesn't use [templating](#template)** @@ -564,6 +579,7 @@ the following entries: * `src`: dotfile path within the `dotpath` (can use `variables` and `dynvariables`, make sure to quote). * `link`: if true dotdrop will create a symlink instead of copying (default *false*). * `cmpignore`: list of pattern to ignore when comparing (enclose in quotes when using wildcards). + * `upignore`: list of pattern to ignore when updating (enclose in quotes when using wildcards). * `actions`: list of action keys that need to be defined in the **actions** entry below. * `trans`: transformation key to apply when installing this dotfile (must be defined in the **trans** entry below). * `trans_write`: transformation key to apply when updating this dotfile (must be defined in the **trans_write** entry below). @@ -578,6 +594,8 @@ the following entries: ignoreempty: cmpignore: - "" + upignore: + - "" actions: - trans: diff --git a/dotdrop/comparator.py b/dotdrop/comparator.py index 6a77713..1d13049 100644 --- a/dotdrop/comparator.py +++ b/dotdrop/comparator.py @@ -7,7 +7,6 @@ handle the comparison of dotfiles and local deployment import os import filecmp -import fnmatch # local imports from dotdrop.logger import Logger @@ -34,7 +33,7 @@ class Comparator: def _comp_file(self, left, right, ignore): """compare a file""" - if self._ignore([left, right], ignore): + if utils.must_ignore([left, right], ignore, debug=self.debug): if self.debug: self.log.dbg('ignoring diff {} and {}'.format(left, right)) return '' @@ -44,7 +43,7 @@ class Comparator: """compare a directory""" if not os.path.exists(right): return '' - if self._ignore([left, right], ignore): + if utils.must_ignore([left, right], ignore, debug=self.debug): if self.debug: self.log.dbg('ignoring diff {} and {}'.format(left, right)) return '' @@ -54,11 +53,13 @@ class Comparator: comp = filecmp.dircmp(left, right) # handle files only in deployed file for i in comp.left_only: - if self._ignore([os.path.join(left, i)], ignore): + if utils.must_ignore([os.path.join(left, i)], + ignore, debug=self.debug): continue ret.append('=> \"{}\" does not exist on local\n'.format(i)) for i in comp.right_only: - if self._ignore([os.path.join(right, i)], ignore): + if utils.must_ignore([os.path.join(right, i)], + ignore, debug=self.debug): continue ret.append('=> \"{}\" does not exist in dotdrop\n'.format(i)) @@ -92,15 +93,3 @@ class Comparator: rshort = os.path.basename(right) diff = '=> diff \"{}\":\n{}'.format(lshort, diff) return diff - - def _ignore(self, paths, ignore): - '''return True if any paths is ignored - not very efficient''' - if not ignore: - return False - for p in paths: - for i in ignore: - if fnmatch.fnmatch(p, i): - if self.debug: - self.log.dbg('ignore match {}'.format(p)) - return True - return False diff --git a/dotdrop/config.py b/dotdrop/config.py index 5cf4f57..0ba8835 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -57,6 +57,7 @@ class Cfg: key_dotfiles_actions = 'actions' key_dotfiles_trans_r = 'trans' key_dotfiles_trans_w = 'trans_write' + key_dotfiles_upignore = 'upignore' # profiles keys key_profiles = 'profiles' @@ -272,15 +273,20 @@ class Cfg: trans_r = None trans_w = None - # parse ignore pattern - ignores = v[self.key_dotfiles_cmpignore] if \ + # parse cmpignore pattern + cmpignores = v[self.key_dotfiles_cmpignore] if \ self.key_dotfiles_cmpignore in v else [] + # parse upignore pattern + upignores = v[self.key_dotfiles_upignore] if \ + self.key_dotfiles_upignore in v else [] + # create new dotfile self.dotfiles[k] = Dotfile(k, dst, src, link=link, actions=actions, trans_r=trans_r, trans_w=trans_w, - cmpignore=ignores, noempty=noempty) + cmpignore=cmpignores, noempty=noempty, + upignore=upignores) # assign dotfiles to each profile for k, v in self.lnk_profiles.items(): diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 68f8a2b..ee8e2f0 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -44,7 +44,8 @@ Usage: dotdrop import [-ldVb] [-c ] [-p ] ... dotdrop compare [-Vb] [-c ] [-p ] [-o ] [-C ...] [-i ...] - dotdrop update [-fdVbk] [-c ] [-p ] [...] + dotdrop update [-fdVbk] [-c ] [-p ] + [-i ...] [...] dotdrop listfiles [-VTb] [-c ] [-p ] dotdrop detail [-Vb] [-c ] [-p ] [...] dotdrop list [-Vb] [-c ] @@ -55,7 +56,7 @@ Options: -p --profile= Specify the profile to use [default: {}]. -c --cfg= Path to the config [default: config.yaml]. -C --file= Path of dotfile to compare. - -i --ignore= Pattern to ignore when diffing. + -i --ignore= Pattern to ignore. -o --dopts= Diff options [default: ]. -n --nodiff Do not diff when installing. -t --temp Install to a temporary directory for review. @@ -209,11 +210,12 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): return same -def cmd_update(opts, conf, paths, iskey=False): +def cmd_update(opts, conf, paths, iskey=False, ignore=[]): """update the dotfile(s) from path(s) or key(s)""" ret = True updater = Updater(conf, opts['dotpath'], opts['dry'], - opts['safe'], iskey=iskey, debug=opts['debug']) + opts['safe'], iskey=iskey, + debug=opts['debug'], ignore=[]) if not iskey: # update paths if opts['debug']: @@ -494,7 +496,8 @@ def main(): if opts['debug']: LOG.dbg('running cmd: update') iskey = args['--key'] - ret = cmd_update(opts, conf, args[''], iskey=iskey) + ret = cmd_update(opts, conf, args[''], iskey=iskey, + ignore=args['--ignore']) elif args['detail']: # detail files diff --git a/dotdrop/dotfile.py b/dotdrop/dotfile.py index 45e14ee..210aa9c 100644 --- a/dotdrop/dotfile.py +++ b/dotdrop/dotfile.py @@ -10,7 +10,8 @@ class Dotfile: def __init__(self, key, dst, src, actions={}, trans_r=None, trans_w=None, - link=False, cmpignore=[], noempty=False): + link=False, cmpignore=[], noempty=False, + upignore=[]): # key of dotfile in the config self.key = key # path where to install this dotfile @@ -29,6 +30,8 @@ class Dotfile: self.cmpignore = cmpignore # do not deploy empty file self.noempty = noempty + # pattern to ignore when updating + self.upignore = upignore def __str__(self): msg = 'key:\"{}\", src:\"{}\", dst:\"{}\", link:\"{}\"' diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 8a3e1a1..46a2d40 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -21,13 +21,14 @@ TILD = '~' class Updater: def __init__(self, conf, dotpath, dry, safe, - iskey=False, debug=False): + iskey=False, debug=False, ignore=[]): self.conf = conf self.dotpath = dotpath self.dry = dry self.safe = safe self.iskey = iskey self.debug = debug + self.ignore = ignore self.log = Logger() def update_path(self, path, profile): @@ -58,9 +59,16 @@ class Updater: """update dotfile from file pointed by path""" ret = False new_path = None + self.ignores = list(set(self.ignore + dotfile.upignore)) + if self.debug: + self.log.dbg('ignore pattern(s): {}'.format(self.ignores)) + left = os.path.expanduser(path) right = os.path.join(self.conf.abs_or_rel(self.dotpath), dotfile.src) right = os.path.expanduser(right) + + if self._ignore([left, right]): + return True if dotfile.trans_w: # apply write transformation if any new_path = self._apply_trans_w(path, dotfile) @@ -137,6 +145,8 @@ class Updater: def _handle_file(self, left, right, compare=True): """sync left (deployed file) and right (dotdrop dotfile)""" + if self._ignore([left, right]): + return True if self.debug: self.log.dbg('update for file {} and {}'.format(left, right)) if self._is_template(right): @@ -169,6 +179,8 @@ class Updater: # paths must be absolute (no tildes) left = os.path.expanduser(left) right = os.path.expanduser(right) + if self._ignore([left, right]): + return True # find the differences diff = filecmp.dircmp(left, right, ignore=None) # handle directories diff @@ -179,6 +191,8 @@ class Updater: left, right = diff.left, diff.right if self.debug: self.log.dbg('sync dir {} to {}'.format(left, right)) + if self._ignore([left, right]): + return True # create dirs that don't exist in dotdrop for toadd in diff.left_only: @@ -188,6 +202,8 @@ class Updater: continue # match to dotdrop dotpath new = os.path.join(right, toadd) + if self._ignore([exist, new]): + continue if self.dry: self.log.dry('would cp -r {} {}'.format(exist, new)) continue @@ -202,6 +218,8 @@ class Updater: if not os.path.isdir(old): # ignore files for now continue + if self._ignore([old]): + continue if self.dry: self.log.dry('would rm -r {}'.format(old)) continue @@ -219,6 +237,8 @@ class Updater: for f in fdiff: fleft = os.path.join(left, f) fright = os.path.join(right, f) + if self._ignore([fleft, fright]): + continue if self.dry: self.log.dry('would cp {} {}'.format(fleft, fright)) continue @@ -233,6 +253,8 @@ class Updater: # ignore dirs, done above continue new = os.path.join(right, toadd) + if self._ignore([exist, new]): + continue if self.dry: self.log.dry('would cp {} {}'.format(exist, new)) continue @@ -248,6 +270,8 @@ class Updater: if os.path.isdir(new): # ignore dirs, done above continue + if self._ignore([new]): + continue if self.dry: self.log.dry('would rm {}'.format(new)) continue @@ -275,3 +299,10 @@ class Updater: if self.safe and not self.log.ask(msg): return False return True + + def _ignore(self, paths): + if utils.must_ignore(paths, self.ignores, debug=self.debug): + if self.debug: + self.log.dbg('ignoring update for {}'.format(paths)) + return True + return False diff --git a/dotdrop/utils.py b/dotdrop/utils.py index b88780b..c37c952 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -10,6 +10,7 @@ import tempfile import os import uuid import shlex +import fnmatch from shutil import rmtree # local import @@ -109,3 +110,16 @@ def strip_home(path): if path.startswith(home): path = path[len(home):] return path + + +def must_ignore(paths, ignores, debug=False): + """return true if any paths in list matches any ignore patterns""" + if not ignores: + return False + for p in paths: + for i in ignores: + if fnmatch.fnmatch(p, i): + if debug: + LOG.dbg('ignore \"{}\" match: {}'.format(i, p)) + return True + return False diff --git a/tests-ng/update-ignore.sh b/tests-ng/update-ignore.sh new file mode 100755 index 0000000..077bdea --- /dev/null +++ b/tests-ng/update-ignore.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2017, deadc0de6 +# +# test ignore update +# 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" + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "\e[96m\e[1m==> RUNNING $(basename $BASH_SOURCE) <==\e[0m" + +################################################################ +# this is the test +################################################################ + +# dotdrop directory +tmps=`mktemp -d` +dt="${tmps}/dotfiles" +mkdir -p ${dt} +mkdir -p ${dt}/a/{b,c} +echo 'a' > ${dt}/a/b/abfile +echo 'a' > ${dt}/a/c/acfile + +# fs dotfiles +tmpd=`mktemp -d` +cp -r ${dt}/a ${tmpd}/ + +# create the config file +cfg="${tmps}/config.yaml" +cat > ${cfg} << _EOF +config: + backup: false + create: true + dotpath: dotfiles +dotfiles: + f_abc: + dst: ${tmpd}/a + src: a + upignore: + - "*/cfile" + - "*/newfile" + - "*/newdir" +profiles: + p1: + dotfiles: + - f_abc +_EOF +cat ${cfg} + +#tree ${dt} + +# edit/add files +echo "[+] edit/add files" +touch ${tmpd}/a/newfile +echo 'b' > ${tmpd}/a/c/acfile +mkdir -p ${tmpd}/a/newdir/b +touch ${tmpd}/a/newdir/b/c + +#tree ${tmpd}/a + +# update +echo "[+] update" +cd ${ddpath} | ${bin} update -f -c ${cfg} --verbose --profile=p1 --key f_abc + +#tree ${dt} + +# check files haven't been updated +grep 'b' ${dt}/a/c/acfile >/dev/null +[ -e ${dt}/a/newfile ] && exit 1 + +## CLEANING +rm -rf ${tmps} ${tmpd} + +echo "OK" +exit 0