From 7f6ab70b3b1f6befe51bfd2919d3a0dabd9abe9b Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 29 Dec 2018 23:32:44 +0100 Subject: [PATCH 1/9] fix normpath for #76 --- dotdrop/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 8b6a71a..080511b 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -64,7 +64,7 @@ class Installer: if not os.path.exists(src): self.log.err('source dotfile does not exist: {}'.format(src)) return [] - dst = os.path.expanduser(dst).rstrip(os.sep) + dst = os.path.normpath(os.path.expanduser(dst)) if self.totemp: # ignore actions return self.install(templater, src, dst, actions=[]) From 6d4526f57e14ff70e31a763756c01a1547187530 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 15:54:46 +0100 Subject: [PATCH 2/9] update compare output --- dotdrop/comparator.py | 6 +++--- dotdrop/dotdrop.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/dotdrop/comparator.py b/dotdrop/comparator.py index 9fa3244..6a77713 100644 --- a/dotdrop/comparator.py +++ b/dotdrop/comparator.py @@ -56,11 +56,11 @@ class Comparator: for i in comp.left_only: if self._ignore([os.path.join(left, i)], ignore): continue - ret.append('only in left: \"{}\"\n'.format(i)) + 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): continue - ret.append('only in right: \"{}\"\n'.format(i)) + ret.append('=> \"{}\" does not exist in dotdrop\n'.format(i)) # same left and right but different type funny = comp.common_funny @@ -90,7 +90,7 @@ class Comparator: if header: lshort = os.path.basename(left) rshort = os.path.basename(right) - diff = 'diff \"{}\":\n{}'.format(lshort, diff) + diff = '=> diff \"{}\":\n{}'.format(lshort, diff) return diff def _ignore(self, paths, ignore): diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index ea37dd9..395e818 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -168,7 +168,10 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): LOG.dbg('comparing {}'.format(dotfile)) src = dotfile.src if not os.path.lexists(os.path.expanduser(dotfile.dst)): - LOG.emph('\"{}\" does not exist on local\n'.format(dotfile.dst)) + line = '=> compare {}: \"{}\" does not exist on local' + LOG.log(line.format(dotfile.key, dotfile.dst)) + same = False + continue tmpsrc = None if dotfile.trans_r: @@ -176,12 +179,14 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): tmpsrc = apply_trans(opts, dotfile) if not tmpsrc: # could not apply trans + same = False continue src = tmpsrc # install dotfile to temporary dir ret, insttmp = inst.install_to_temp(t, tmp, src, dotfile.dst) if not ret: # failed to install to tmp + same = False continue ignores = list(set(ignore + dotfile.cmpignore)) diff = comp.compare(insttmp, dotfile.dst, ignore=ignores) @@ -192,12 +197,12 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): remove(tmpsrc) if diff == '': if opts['debug']: - LOG.dbg('diffing \"{}\" VS \"{}\"'.format(dotfile.key, - dotfile.dst)) + line = '=> compare {}: diffing with \"{}\"' + LOG.dbg(line.format(dotfile.key, dotfile.dst)) LOG.dbg('same file') else: - LOG.log('diffing \"{}\" VS \"{}\"'.format(dotfile.key, - dotfile.dst)) + line = '=> compare {}: diffing with \"{}\"' + LOG.log(line.format(dotfile.key, dotfile.dst)) LOG.emph(diff) same = False From 0a5b38090d026ff8d65c63dd37ecd2a32762c534 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 15:54:55 +0100 Subject: [PATCH 3/9] test for pycodestyle --- tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests.sh b/tests.sh index 77c7496..61f3589 100755 --- a/tests.sh +++ b/tests.sh @@ -6,6 +6,8 @@ set -ev # PEP8 tests +which pycodestyle 2>/dev/null +[ "$?" != "0" ] && echo "Install pycodestyle" && exit 1 pycodestyle --ignore=W605 dotdrop/ pycodestyle tests/ pycodestyle scripts/ From 91b1b7f7fe03809f6e539dae2c6b231d6f5d32cb Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 17:42:23 +0100 Subject: [PATCH 4/9] -l inverts the value of link_by_default --- dotdrop/dotdrop.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 395e818..717852b 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -61,7 +61,7 @@ Options: -t --temp Install to a temporary directory for review. -T --template Only template dotfiles. -D --showdiff Show a diff before overwriting. - -l --link Import and link. + -l --inv-link Invert the value of "link_by_default" when importing. -f --force Do not warn if exists. -k --key Treat as a dotfile key. -V --verbose Be verbose. @@ -256,7 +256,9 @@ def cmd_importer(opts, conf, paths): # create a new dotfile dotfile = Dotfile('', dst, src) - linkit = opts['link'] or opts['link_by_default'] + linkit = opts['link_by_default'] + if opts['link']: + linkit = not linkit if opts['debug']: LOG.dbg('new dotfile: {}'.format(dotfile)) @@ -431,7 +433,7 @@ def main(): opts['profile'] = args['--profile'] opts['safe'] = not args['--force'] opts['installdiff'] = not args['--nodiff'] - opts['link'] = args['--link'] + opts['link'] = args['--inv-link'] opts['debug'] = args['--verbose'] opts['variables'] = conf.get_variables() opts['showdiff'] = opts['showdiff'] or args['--showdiff'] From 7ba8ed32b826a1d43d1f5efb2129af02dd379a49 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 19:07:28 +0100 Subject: [PATCH 5/9] adding profile specific (dyn)variables for #81 --- README.md | 30 +++++++ dotdrop/config.py | 33 ++++++-- dotdrop/dotdrop.py | 2 +- tests-ng/profile-dynvariables.sh | 130 +++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 7 deletions(-) create mode 100755 tests-ng/profile-dynvariables.sh diff --git a/README.md b/README.md index 75fd6e5..72b135b 100644 --- a/README.md +++ b/README.md @@ -588,6 +588,8 @@ the following entries: need to be managed * `dotfiles`: the dotfiles associated to this profile * `include`: include all dotfiles from another profile (optional) + * `variables`: profile specific variables (see [Variables](#variables)) + * `dynvariables`: profile specific interpreted variables (see [Interpreted variables](#interpreted-variables)) ```yaml : @@ -599,6 +601,10 @@ the following entries: include: - - ... + variables: + : + dynvariables: + : ``` * **actions** entry (optional): a list of action (see [Use actions](#use-actions)) @@ -715,6 +721,7 @@ For example in the config file: ```yaml variables: var1: some variable content + var2: some other content ``` These can then be used in any template with @@ -722,6 +729,26 @@ These can then be used in any template with {{@@ var1 @@}} ``` +Profile variables will take precedence over globally defined variables what +means that you could do something like this: +``` +variables: + git_email: home@email.com +dotfiles: + f_gitconfig: + dst: ~/.gitconfig + src: gitconfig +profiles: + work: + dotfiles: + - f_gitconfig + variables: + git_email: work@email.com + private: + dotfiles: + - f_gitconfig +``` + ## Interpreted variables It is also possible to have *dynamic* variables in the sense that their @@ -740,6 +767,9 @@ These can be used as any variables in the templates {{@@ dvar1 @@}} ``` +As for variables (see [Variables](#variables)) profile dynvariables will take +precedence over globally defined dynvariables. + ## Environment variables It's possible to access environment variables inside the templates. diff --git a/dotdrop/config.py b/dotdrop/config.py index 85c1510..5cf4f57 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -44,7 +44,7 @@ class Cfg: # template variables key_variables = 'variables' - # shell variable + # shell variables key_dynvariables = 'dynvariables' # dotfiles keys @@ -111,13 +111,14 @@ class Cfg: # represents all dotfiles per profile by profile key # NOT linked inside the yaml dict (self.content) self.prodots = {} + if not self._load_file(): raise ValueError('config is not valid') def eval_dotfiles(self, profile, debug=False): - """resolve dotfiles src/dst templates""" + """resolve dotfiles src/dst templating""" t = Templategen(profile=profile, - variables=self.get_variables(), + variables=self.get_variables(profile), debug=debug) for d in self.get_dotfiles(profile): d.src = t.generate_string(d.src) @@ -600,15 +601,35 @@ class Cfg: """return all defined settings""" return self.lnk_settings.copy() - def get_variables(self): + def get_variables(self, profile): + """return the variables for this profile""" variables = {} + + # global variables if self.key_variables in self.content: variables.update(self.content[self.key_variables]) + + # global dynvariables if self.key_dynvariables in self.content: # interpret dynamic variables dynvars = self.content[self.key_dynvariables] - for key, cmd in dynvars.items(): - variables[key] = shell(cmd) + for k, v in dynvars.items(): + variables[k] = shell(v) + + if profile not in self.lnk_profiles: + return variables + + # profile variables + var = self.lnk_profiles[profile] + if self.key_variables in var.keys(): + for k, v in var[self.key_variables].items(): + variables[k] = v + + # profile dynvariables + if self.key_dynvariables in var.keys(): + for k, v in var[self.key_dynvariables].items(): + variables[k] = shell(v) + return variables def dump(self): diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 717852b..68f8a2b 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -435,7 +435,7 @@ def main(): opts['installdiff'] = not args['--nodiff'] opts['link'] = args['--inv-link'] opts['debug'] = args['--verbose'] - opts['variables'] = conf.get_variables() + opts['variables'] = conf.get_variables(opts['profile']) opts['showdiff'] = opts['showdiff'] or args['--showdiff'] if opts['debug']: diff --git a/tests-ng/profile-dynvariables.sh b/tests-ng/profile-dynvariables.sh new file mode 100755 index 0000000..1940bb6 --- /dev/null +++ b/tests-ng/profile-dynvariables.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2017, deadc0de6 +# +# test variables per profile +# 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 +################################################################ + +# the dotfile source +tmps=`mktemp -d` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d` +#echo "dotfile destination: ${tmpd}" + +# create a shell script +export TESTENV="this is my global testenv" +scr=`mktemp` +chmod +x ${scr} +echo -e "#!/bin/bash\necho $TESTENV\n" >> ${scr} + +export TESTENV2="this is my profile testenv" +scr2=`mktemp` +chmod +x ${scr2} +echo -e "#!/bin/bash\necho $TESTENV2\n" >> ${scr2} + +# create the config file +cfg="${tmps}/config.yaml" + +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles +variables: + gvar1: "global1" + gvar2: "global2" +dynvariables: + gdvar1: head -1 /proc/meminfo + gdvar2: "echo 'this is some test' | rev | tr ' ' ','" + gdvar3: ${scr} +dotfiles: + f_abc: + dst: ${tmpd}/abc + src: abc +profiles: + p1: + variables: + gvar1: "local1" + lvar1: "local2" + dynvariables: + gdvar3: ${scr2} + pdvar1: "echo 'abc' | rev" + dotfiles: + - f_abc +_EOF +cat ${cfg} + +# create the dotfile +echo "===================" > ${tmps}/dotfiles/abc +echo "{{@@ gvar1 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ gvar2 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ gdvar1 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ gdvar2 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ gdvar3 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ lvar1 @@}}" >> ${tmps}/dotfiles/abc +echo "{{@@ pdvar1 @@}}" >> ${tmps}/dotfiles/abc +echo "===================" >> ${tmps}/dotfiles/abc + +# install +cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V + +#cat ${tmpd}/abc + +# test variables +grep '^local1' ${tmpd}/abc >/dev/null +grep '^global2' ${tmpd}/abc >/dev/null +grep '^local2' ${tmpd}/abc >/dev/null +# test dynvariables +grep "^MemTotal" ${tmpd}/abc >/dev/null +grep '^tset,emos,si,siht' ${tmpd}/abc >/dev/null +grep "^${TESTENV2}" ${tmpd}/abc > /dev/null +grep "^cba" ${tmpd}/abc >/dev/null + +#cat ${tmpd}/abc + +## CLEANING +rm -rf ${tmps} ${tmpd} ${scr} ${scr2} + +echo "OK" +exit 0 From 2ec1516be07c7181450f6a9307fecb02fed0469a Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 19:24:05 +0100 Subject: [PATCH 6/9] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72b135b..25bc3a5 100644 --- a/README.md +++ b/README.md @@ -602,9 +602,9 @@ the following entries: - - ... variables: - : + : dynvariables: - : + : ``` * **actions** entry (optional): a list of action (see [Use actions](#use-actions)) From 67b8fee18c02385e8b852632eb9d848f9c7bb178 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 19:25:36 +0100 Subject: [PATCH 7/9] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25bc3a5..066f1b8 100644 --- a/README.md +++ b/README.md @@ -731,7 +731,7 @@ These can then be used in any template with Profile variables will take precedence over globally defined variables what means that you could do something like this: -``` +```yaml variables: git_email: home@email.com dotfiles: From f78e3126f5ab826e9058fb5880e5e0a1ea6ed1b5 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 20:15:24 +0100 Subject: [PATCH 8/9] handle empty file as text file --- dotdrop/templategen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotdrop/templategen.py b/dotdrop/templategen.py index f911261..6b7c8e9 100644 --- a/dotdrop/templategen.py +++ b/dotdrop/templategen.py @@ -70,7 +70,7 @@ class Templategen: filetype = filetype.strip() if self.debug: self.log.dbg('\"{}\" filetype: {}'.format(src, filetype)) - istext = 'text' in filetype + istext = 'text' in filetype or 'empty' in filetype if self.debug: self.log.dbg('\"{}\" is text: {}'.format(src, istext)) if not istext: From 0162412d4c469ba439c132802a70cd1ab056130a Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 17 Jan 2019 21:22:12 +0100 Subject: [PATCH 9/9] implement upignore for #79 --- README.md | 20 ++++++- dotdrop/comparator.py | 23 +++----- dotdrop/config.py | 12 +++-- dotdrop/dotdrop.py | 13 +++-- dotdrop/dotfile.py | 5 +- dotdrop/updater.py | 33 +++++++++++- dotdrop/utils.py | 14 +++++ tests-ng/update-ignore.sh | 107 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 199 insertions(+), 28 deletions(-) create mode 100755 tests-ng/update-ignore.sh 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