diff --git a/dotdrop/cfg_aggregator.py b/dotdrop/cfg_aggregator.py index 9063b57..2141f58 100644 --- a/dotdrop/cfg_aggregator.py +++ b/dotdrop/cfg_aggregator.py @@ -147,6 +147,16 @@ class CfgAggregator: """remove this dotfile from this profile""" return self.cfgyaml.del_dotfile_from_profile(dotfile.key, profile.key) + def _create_new_dotfile(self, src, dst, link): + """create a new dotfile""" + # get a new dotfile with a unique key + key = self._get_new_dotfile_key(dst) + if self.debug: + self.log.dbg('new dotfile key: {}'.format(key)) + # add the dotfile + self.cfgyaml.add_dotfile(key, src, dst, link) + return Dotfile(key, dst, src) + def new(self, src, dst, link): """ import a new dotfile @@ -155,34 +165,31 @@ class CfgAggregator: @link: LinkType """ dst = self.path_to_dotfile_dst(dst) - - dotfile = self.get_dotfile_by_dst(dst) + dotfile = self.get_dotfile_by_src_dst(src, dst) if not dotfile: - # get a new dotfile with a unique key - key = self._get_new_dotfile_key(dst) - if self.debug: - self.log.dbg('new dotfile key: {}'.format(key)) - # add the dotfile - self.cfgyaml.add_dotfile(key, src, dst, link) - dotfile = Dotfile(key, dst, src) + dotfile = self._create_new_dotfile(src, dst, link) key = dotfile.key ret = self.cfgyaml.add_dotfile_to_profile(key, self.profile_key) - if self.debug: + if ret and self.debug: msg = 'new dotfile {} to profile {}' self.log.dbg(msg.format(key, self.profile_key)) - # reload self.cfgyaml.save() - if self.debug: - self.log.dbg('reloading config') - self._load() + if ret: + # reload + if self.debug: + self.log.dbg('reloading config') + olddebug = self.debug + self.debug = False + self._load() + self.debug = olddebug return ret def _get_new_dotfile_key(self, dst): """return a new unique dotfile key""" path = os.path.expanduser(dst) - existing_keys = [x.key for x in self.dotfiles] + existing_keys = self.cfgyaml.get_all_dotfile_keys() if self.settings.longkey: return self._get_long_key(path, existing_keys) return self._get_short_key(path, existing_keys) @@ -257,11 +264,28 @@ class CfgAggregator: return path def get_dotfile_by_dst(self, dst): - """get a dotfile by dst""" + """ + get a list of dotfiles by dst + @dst: dotfile dst (on filesystem) + """ + dotfiles = [] dst = self._norm_path(dst) for d in self.dotfiles: left = self._norm_path(d.dst) if left == dst: + dotfiles.append(d) + return dotfiles + + def get_dotfile_by_src_dst(self, src, dst): + """ + get a dotfile by src and dst + @src: dotfile src (in dotpath) + @dst: dotfile dst (on filesystem) + """ + src = self.cfgyaml.resolve_dotfile_src(src) + dotfiles = self.get_dotfile_by_dst(dst) + for d in dotfiles: + if d.src == src: return d return None diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index c09992b..01f1a18 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -818,6 +818,10 @@ class CfgYaml: self.dirty = True return self.dirty + def get_all_dotfile_keys(self): + """return all existing dotfile keys""" + return self.dotfiles.keys() + def add_dotfile(self, key, src, dst, link): """add a new dotfile""" if key in self.dotfiles.keys(): diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index f499ae4..4bddfe6 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -341,7 +341,17 @@ def cmd_importer(o): continue dst = path.rstrip(os.sep) dst = os.path.abspath(dst) + src = strip_home(dst) + if o.import_as: + # handle import as + src = os.path.expanduser(o.import_as) + src = src.rstrip(os.sep) + src = os.path.abspath(src) + src = strip_home(src) + if o.debug: + LOG.dbg('import src for {} as {}'.format(dst, src)) + strip = '.' + os.sep if o.keepdot: strip = os.sep @@ -358,6 +368,25 @@ def cmd_importer(o): if o.debug: LOG.dbg('import dotfile: src:{} dst:{}'.format(src, dst)) + # test no other dotfile exists with same + # dst for this profile but different src + dfs = o.conf.get_dotfile_by_dst(dst) + if dfs: + invalid = False + for df in dfs: + profiles = o.conf.get_profiles_by_dotfile_key(df.key) + profiles = [x.key for x in profiles] + if o.profile in profiles and \ + not o.conf.get_dotfile_by_src_dst(src, dst): + # same profile + # different src + LOG.err('duplicate dotfile for this profile') + ret = False + invalid = True + break + if invalid: + continue + # prepare hierarchy for dotfile srcf = os.path.join(o.dotpath, src) overwrite = not os.path.exists(srcf) @@ -481,52 +510,54 @@ def cmd_remove(o): for key in paths: if not iskey: # by path - dotfile = o.conf.get_dotfile_by_dst(key) - if not dotfile: + dotfiles = o.conf.get_dotfile_by_dst(key) + if not dotfiles: LOG.warn('{} ignored, does not exist'.format(key)) continue - k = dotfile.key else: # by key dotfile = o.conf.get_dotfile(key) if not dotfile: LOG.warn('{} ignored, does not exist'.format(key)) continue - k = key + dotfiles = [dotfile] - # ignore if uses any type of link - if dotfile.link != LinkTypes.NOLINK: - LOG.warn('dotfile uses link, remove manually') - continue + for dotfile in dotfiles: + k = dotfile.key + # ignore if uses any type of link + if dotfile.link != LinkTypes.NOLINK: + LOG.warn('dotfile uses link, remove manually') + continue - if o.debug: - LOG.dbg('removing {}'.format(key)) + if o.debug: + LOG.dbg('removing {}'.format(key)) - # make sure is part of the profile - if dotfile.key not in [d.key for d in o.dotfiles]: - LOG.warn('{} ignored, not associated to this profile'.format(key)) - continue - profiles = o.conf.get_profiles_by_dotfile_key(k) - pkeys = ','.join([p.key for p in profiles]) - if o.dry: - LOG.dry('would remove {} from {}'.format(dotfile, pkeys)) - continue - msg = 'Remove \"{}\" from all these profiles: {}'.format(k, pkeys) - if o.safe and not LOG.ask(msg): - return False - if o.debug: - LOG.dbg('remove dotfile: {}'.format(dotfile)) - - for profile in profiles: - if not o.conf.del_dotfile_from_profile(dotfile, profile): + # make sure is part of the profile + if dotfile.key not in [d.key for d in o.dotfiles]: + msg = '{} ignored, not associated to this profile' + LOG.warn(msg.format(key)) + continue + profiles = o.conf.get_profiles_by_dotfile_key(k) + pkeys = ','.join([p.key for p in profiles]) + if o.dry: + LOG.dry('would remove {} from {}'.format(dotfile, pkeys)) + continue + msg = 'Remove \"{}\" from all these profiles: {}'.format(k, pkeys) + if o.safe and not LOG.ask(msg): return False - if not o.conf.del_dotfile(dotfile): - return False + if o.debug: + LOG.dbg('remove dotfile: {}'.format(dotfile)) - # remove dotfile from dotpath - dtpath = os.path.join(o.dotpath, dotfile.src) - remove(dtpath) - removed.append(dotfile.key) + for profile in profiles: + if not o.conf.del_dotfile_from_profile(dotfile, profile): + return False + if not o.conf.del_dotfile(dotfile): + return False + + # remove dotfile from dotpath + dtpath = os.path.join(o.dotpath, dotfile.src) + remove(dtpath) + removed.append(dotfile.key) if o.dry: LOG.dry('new config file would be:') diff --git a/dotdrop/options.py b/dotdrop/options.py index c103907..274c083 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -52,7 +52,7 @@ USAGE = """ Usage: dotdrop install [-VbtfndDa] [-c ] [-p ] [...] - dotdrop import [-Vbdf] [-c ] [-p ] + dotdrop import [-Vbdf] [-c ] [-p ] [-s ] [-l ] ... dotdrop compare [-Vb] [-c ] [-p ] [-C ...] [-i ...] @@ -72,6 +72,7 @@ Options: -i --ignore= Pattern to ignore. -l --link= Link option (nolink|link|link_children). -p --profile= Specify the profile to use [default: {}]. + -s --as= Import as a different path from actual path. -b --no-banner Do not display the banner. -d --dry Dry run. -D --showdiff Show a diff before overwriting. @@ -237,6 +238,7 @@ class Options(AttrMonitor): self.compare_ignore = uniq_list(self.compare_ignore) # "import" specifics self.import_path = self.args[''] + self.import_as = self.args['--as'] # "update" specifics self.update_path = self.args[''] self.update_iskey = self.args['--key'] diff --git a/dotdrop/updater.py b/dotdrop/updater.py index f307162..188882f 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -61,12 +61,21 @@ class Updater: if not os.path.lexists(path): self.log.err('\"{}\" does not exist!'.format(path)) return False - dotfile = self.dotfile_dst_getter(path) - if not dotfile: + dotfiles = self.dotfile_dst_getter(path) + if not dotfiles: return False - if self.debug: - self.log.dbg('updating {} from path \"{}\"'.format(dotfile, path)) - return self._update(path, dotfile) + for dotfile in dotfiles: + if not dotfile: + msg = 'invalid dotfile for update: {}' + self.log.err(msg.format(dotfile.key)) + return False + + if self.debug: + msg = 'updating {} from path \"{}\"' + self.log.dbg(msg.format(dotfile, path)) + if not self._update(path, dotfile): + return False + return True def update_key(self, key): """update the dotfile referenced by key""" diff --git a/tests-ng/import-as.sh b/tests-ng/import-as.sh new file mode 100755 index 0000000..34d7f98 --- /dev/null +++ b/tests-ng/import-as.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2019, deadc0de6 +# +# test basic import +# + +# 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 "$(tput setaf 6)==> RUNNING $(basename $BASH_SOURCE) <==$(tput sgr0)" + +################################################################ +# this is the test +################################################################ + +clean() +{ + rm -rf ${tmps} ${tmpd} ~/.dotdrop.test ~/.dotdrop-dotfiles-test +} + +# the dotfile source +tmps=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +#echo "dotfile destination: ${tmpd}" + +# create the dotfile +mkdir -p ${tmpd}/adir +echo "adir/file1" > ${tmpd}/adir/file1 +echo "adir/fil2" > ${tmpd}/adir/file2 +echo "file3" > ${tmpd}/file3 + +# create the config file +cfg="${tmps}/config.yaml" + +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles +dotfiles: +profiles: +_EOF +#cat ${cfg} + +# import +cd ${ddpath} | ${bin} import -c ${cfg} -p p1 -V ${tmpd}/adir +cd ${ddpath} | ${bin} import -c ${cfg} -p p1 -V ${tmpd}/file3 + +echo "import --as dotfiles" +cd ${ddpath} | ${bin} import -c ${cfg} -p p2 -V ${tmpd}/adir --as ~/config/adir +cd ${ddpath} | ${bin} import -c ${cfg} -p p2 -V ${tmpd}/file3 --as ~/config2/file3 + +cat ${cfg} + +set +e +cd ${ddpath} | ${bin} import -c ${cfg} -p p2 -V ${tmpd}/adir --as ~/config/should_not && echo "dual dst imported" && exit 1 +set -e +cat ${cfg} | grep should_not && echo "dual dst imported" && exit 1 + +cat ${cfg} + +echo "ensure exists and is not link" +[ ! -d ${tmps}/dotfiles/${tmpd}/adir ] && echo "not a directory" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/adir/file1 ] && echo "not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/adir/file2 ] && echo "not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/file3 ] && echo "not a file" && exit 1 + +echo "ensure --as are correctly imported" +[ ! -d ${tmps}/dotfiles/config/adir ] && echo "not a directory" && exit 1 +[ ! -e ${tmps}/dotfiles/config/adir/file1 ] && echo "not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/config/adir/file2 ] && echo "not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/config2/file3 ] && echo "not a file" && exit 1 + +cat ${cfg} | grep ${tmpd}/adir >/dev/null 2>&1 +cat ${cfg} | grep ${tmpd}/file3 >/dev/null 2>&1 + +cat ${cfg} | grep config/adir >/dev/null 2>&1 +cat ${cfg} | grep config2/file3 >/dev/null 2>&1 + +nb=`cat ${cfg} | grep d_adir | wc -l` +[ "${nb}" != "2" ] && echo 'bad config1' && exit 1 +nb=`cat ${cfg} | grep f_file3 | wc -l` +[ "${nb}" != "2" ] && echo 'bad config2' && exit 1 + +cat ${cfg} | grep "src: config/adir" || exit 1 +cat ${cfg} | grep "src: config2/file3" || exit 1 + +# test import from sub in home +mkdir -p ~/.dotdrop-dotfiles-test/{dotfiles,config} +cfg=~/.dotdrop-dotfiles-test/config/config.yaml +echo 'remove-me' > ~/.dotdrop.test +cat > ${cfg} << _EOF +config: + backup: true + banner: true + create: true + dotpath: ~/.dotdrop-dotfiles-test/dotfiles + keepdot: false + link_dotfile_default: nolink + link_on_import: nolink + longkey: true +dotfiles: +profiles: +_EOF + +cd ${ddpath} | ${bin} import -b -c ${cfg} -p test -V ~/.dotdrop.test --as=~/.whatever +#cat ${cfg} + +[ ! -e ~/.dotdrop-dotfiles-test/dotfiles/whatever ] && clean && echo 'tild imported' && exit 1 +cat ${cfg} | grep '~/.whatever' && clean && echo 'import with tild failed' && exit 1 + +## CLEANING +clean + +echo "OK" +exit 0 diff --git a/tests-ng/re-import.sh b/tests-ng/re-import.sh index 5a95fa7..5974c31 100755 --- a/tests-ng/re-import.sh +++ b/tests-ng/re-import.sh @@ -44,6 +44,11 @@ echo -e "$(tput setaf 6)==> RUNNING $(basename $BASH_SOURCE) <==$(tput sgr0)" # this is the test ################################################################ +clean() +{ + rm -rf ${tmps} ${tmpd} ~/.dotdrop-test +} + # the dotfile source tmps=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` mkdir -p ${tmps}/dotfiles @@ -73,11 +78,11 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ${tmpd}/testfile cat ${cfg} # ensure exists and is not link -[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep ${tmpd}/testfile >/dev/null 2>&1 grep 'original' ${tmps}/dotfiles/${tmpd}/testfile nb=`cat ${cfg} | grep ${tmpd}/testfile | wc -l` -[ "${nb}" != "1" ] && echo 'not 1 entry' && exit 1 +[ "${nb}" != "1" ] && echo 'not 1 entry' && clean && exit 1 # re-import without changing echo "[+] re-import without changes" @@ -85,11 +90,11 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ${tmpd}/testfile cat ${cfg} # test is only once -[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep ${tmpd}/testfile >/dev/null 2>&1 grep 'original' ${tmps}/dotfiles/${tmpd}/testfile nb=`cat ${cfg} | grep ${tmpd}/testfile | wc -l` -[ "${nb}" != "1" ] && echo 'two entries!' && exit 1 +[ "${nb}" != "1" ] && echo 'two entries!' && clean && exit 1 # re-import with changes echo "[+] re-import with changes" @@ -98,11 +103,11 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ${tmpd}/testfile cat ${cfg} # test is only once -[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/testfile ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep ${tmpd}/testfile >/dev/null 2>&1 grep 'modified' ${tmps}/dotfiles/${tmpd}/testfile nb=`cat ${cfg} | grep ${tmpd}/testfile | wc -l` -[ "${nb}" != "1" ] && echo 'two entries!' && exit 1 +[ "${nb}" != "1" ] && echo 'two entries!' && clean && exit 1 # ################################################### @@ -113,11 +118,11 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ~/.dotdrop.test cat ${cfg} # ensure exists and is not link -[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && exit 1 +[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep "~/.dotdrop.test" >/dev/null 2>&1 grep 'original' ${tmps}/dotfiles/dotdrop.test nb=`cat ${cfg} | grep "~/.dotdrop.test" | wc -l` -[ "${nb}" != "1" ] && echo 'not 1 entry' && exit 1 +[ "${nb}" != "1" ] && echo 'not 1 entry' && clean && exit 1 # re-import without changing echo "[+] re-import without changes in home" @@ -125,11 +130,11 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ~/.dotdrop.test cat ${cfg} # test is only once -[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && exit 1 +[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep "~/.dotdrop.test" >/dev/null 2>&1 grep 'original' ${tmps}/dotfiles/dotdrop.test nb=`cat ${cfg} | grep "~/.dotdrop.test" | wc -l` -[ "${nb}" != "1" ] && echo 'two entries!' && exit 1 +[ "${nb}" != "1" ] && echo 'two entries!' && clean && exit 1 # re-import with changes echo "[+] re-import with changes in home" @@ -138,14 +143,14 @@ cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -V ~/.dotdrop.test cat ${cfg} # test is only once -[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && exit 1 +[ ! -e "${tmps}/dotfiles/dotdrop.test" ] && echo "does not exist" && clean && exit 1 cat ${cfg} | grep "~/.dotdrop.test" >/dev/null 2>&1 grep 'modified' ${tmps}/dotfiles/dotdrop.test nb=`cat ${cfg} | grep "~/.dotdrop.test" | wc -l` -[ "${nb}" != "1" ] && echo 'two entries!' && exit 1 +[ "${nb}" != "1" ] && echo 'two entries!' && clean && exit 1 ## CLEANING -rm -rf ${tmps} ${tmpd} ~/.dotdrop-test +clean echo "OK" exit 0 diff --git a/tests/helpers.py b/tests/helpers.py index cc6b958..d8cef2a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -129,6 +129,7 @@ def _fake_args(): args['--show-patch'] = False args['--force-actions'] = False args['--grepable'] = False + args['--as'] = None # cmds args['profiles'] = False args['files'] = False