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 af8eb78..7639a58 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -815,6 +815,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 d9c0712..625a109 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -341,7 +341,16 @@ 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 = o.import_as.rstrip(os.sep) + src = os.path.abspath(src) + src = strip_home(src) + if o.debug: + LOG.dbg('import src for {}: {}'.format(dst, src)) + strip = '.' + os.sep if o.keepdot: strip = os.sep @@ -358,6 +367,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 +509,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..065873d --- /dev/null +++ b/tests-ng/import-as.sh @@ -0,0 +1,122 @@ +#!/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 +################################################################ + +# 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 + +cat ${cfg} + +## CLEANING +rm -rf ${tmps} ${tmpd} + +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