diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index b01253e..3fe14eb 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -46,7 +46,7 @@ Usage: dotdrop import [-ldVb] [-c ] [-p ] ... dotdrop compare [-Vb] [-c ] [-p ] [-o ] [-C ...] [-i ...] - dotdrop update [-fdVb] [-c ] [-p ] ... + dotdrop update [-fdVbk] [-c ] [-p ] [...] dotdrop listfiles [-VTb] [-c ] [-p ] dotdrop list [-Vb] [-c ] dotdrop --help @@ -64,6 +64,7 @@ Options: -D --showdiff Show a diff before overwriting. -l --link Import and link. -f --force Do not warn if exists. + -k --key Treat as a dotfile key. -V --verbose Be verbose. -d --dry Dry run. -b --no-banner Do not display the banner. @@ -203,21 +204,41 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): return same -def cmd_update(opts, conf, paths): - """update the dotfile(s) from path(s)""" +def cmd_update(opts, conf, paths, iskey=False): + """update the dotfile(s) from path(s) or key(s)""" + ret = True updater = Updater(conf, opts['dotpath'], opts['dry'], - opts['safe'], opts['debug']) - for path in paths: - updater.update(path, opts['profile']) + opts['safe'], iskey=iskey, debug=opts['debug']) + if not iskey: + # update paths + if opts['debug']: + LOG.dbg('update by paths: {}'.format(paths)) + for path in paths: + if not updater.update_path(path, opts['profile']): + ret = False + else: + # update keys + keys = paths + if not keys: + # if not provided, take all keys + keys = [d.key for d in conf.get_dotfiles(opts['profile'])] + if opts['debug']: + LOG.dbg('update by keys: {}'.format(keys)) + for key in keys: + if not updater.update_key(key, opts['profile']): + ret = False + return ret def cmd_importer(opts, conf, paths): """import dotfile(s) from paths""" + ret = True home = os.path.expanduser(TILD) cnt = 0 for path in paths: if not os.path.lexists(path): LOG.err('\"{}\" does not exist, ignored!'.format(path)) + ret = False continue dst = path.rstrip(os.sep) dst = os.path.abspath(dst) @@ -243,6 +264,7 @@ def cmd_importer(opts, conf, paths): r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) + ret = False continue cmd = ['cp', '-R', '-L', dst, srcf] if opts['dry']: @@ -253,6 +275,7 @@ def cmd_importer(opts, conf, paths): r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) + ret = False continue if linkit: remove(dst) @@ -269,6 +292,7 @@ def cmd_importer(opts, conf, paths): else: conf.save() LOG.log('\n{} file(s) imported.'.format(cnt)) + return True def cmd_list_profiles(conf): @@ -386,19 +410,27 @@ def main(): if args['list']: # list existing profiles + if opts['debug']: + LOG.dbg('running cmd: list') cmd_list_profiles(conf) elif args['listfiles']: # list files for selected profile + if opts['debug']: + LOG.dbg('running cmd: listfiles') cmd_list_files(opts, conf, templateonly=args['--template']) elif args['install']: # install the dotfiles stored in dotdrop + if opts['debug']: + LOG.dbg('running cmd: install') ret = cmd_install(opts, conf, temporary=args['--temp'], keys=args['']) elif args['compare']: # compare local dotfiles with dotfiles stored in dotdrop + if opts['debug']: + LOG.dbg('running cmd: compare') tmp = get_tmpdir() opts['dopts'] = args['--dopts'] ret = cmd_compare(opts, conf, tmp, focus=args['--file'], @@ -408,11 +440,16 @@ def main(): elif args['import']: # import dotfile(s) - cmd_importer(opts, conf, args['']) + if opts['debug']: + LOG.dbg('running cmd: import') + ret = cmd_importer(opts, conf, args['']) elif args['update']: # update a dotfile - cmd_update(opts, conf, args['']) + if opts['debug']: + LOG.dbg('running cmd: update') + iskey = args['--key'] + ret = cmd_update(opts, conf, args[''], iskey=iskey) except KeyboardInterrupt: LOG.err('interrupted') diff --git a/dotdrop/updater.py b/dotdrop/updater.py index 6151c57..5f25a84 100644 --- a/dotdrop/updater.py +++ b/dotdrop/updater.py @@ -19,32 +19,45 @@ TILD = '~' class Updater: - def __init__(self, conf, dotpath, dry, safe, debug): + def __init__(self, conf, dotpath, dry, safe, + iskey=False, debug=False): self.home = os.path.expanduser(TILD) self.conf = conf self.dotpath = dotpath self.dry = dry self.safe = safe + self.iskey = iskey self.debug = debug self.log = Logger() - def update(self, path, profile): + def update_path(self, path, profile): """update the dotfile installed on path""" if not os.path.lexists(path): self.log.err('\"{}\" does not exist!'.format(path)) return False - left = self._normalize(path) - dotfile = self._get_dotfile(left, profile) + path = self._normalize(path) + dotfile = self._get_dotfile_by_path(path, profile) if not dotfile: return False if self.debug: - self.log.dbg('updating {} from {}'.format(dotfile, path)) + self.log.dbg('updating {} from path \"{}\"'.format(dotfile, path)) + return self._update(path, dotfile) + def update_key(self, key, profile): + """update the dotfile referenced by key""" + dotfile = self._get_dotfile_by_key(key, profile) + if not dotfile: + return False + if self.debug: + self.log.dbg('updating {} from key \"{}\"'.format(dotfile, key)) + path = self._normalize(dotfile.dst) + return self._update(path, dotfile) + + def _update(self, path, dotfile): + """update dotfile from file pointed by path""" + left = os.path.expanduser(path) right = os.path.join(self.conf.abs_dotpath(self.dotpath), dotfile.src) - # expands user - left = os.path.expanduser(left) right = os.path.expanduser(right) - # go through all files and update if os.path.isdir(path): return self._handle_dir(left, right) return self._handle_file(left, right) @@ -60,7 +73,20 @@ class Updater: path = os.path.join(TILD, path) return path - def _get_dotfile(self, path, profile): + def _get_dotfile_by_key(self, key, profile): + """get the dotfile matching this key""" + dotfiles = self.conf.get_dotfiles(profile) + subs = [d for d in dotfiles if d.key == key] + if not subs: + self.log.err('key \"{}\" not found!'.format(path)) + return None + if len(subs) > 1: + found = ','.join([d.src for d in dotfiles]) + self.log.err('multiple dotfiles found: {}'.format(found)) + return None + return subs[0] + + def _get_dotfile_by_path(self, path, profile): """get the dotfile matching this path""" dotfiles = self.conf.get_dotfiles(profile) subs = [d for d in dotfiles if d.dst == path] @@ -114,7 +140,7 @@ class Updater: # find the differences diff = filecmp.dircmp(left, right, ignore=None) # handle directories diff - self._merge_dirs(diff) + return self._merge_dirs(diff) def _merge_dirs(self, diff): """Synchronize directories recursively.""" @@ -123,8 +149,6 @@ class Updater: self.log.dbg('sync dir {} to {}'.format(left, right)) # create dirs that don't exist in dotdrop - if self.debug: - self.log.dbg('handle dirs that do not exist in dotdrop') for toadd in diff.left_only: exist = os.path.join(left, toadd) if not os.path.isdir(exist): @@ -141,8 +165,6 @@ class Updater: shutil.copytree(exist, new) # remove dirs that don't exist in deployed version - if self.debug: - self.log.dbg('remove dirs that do not exist in deployed version') for toremove in diff.right_only: old = os.path.join(right, toremove) if not os.path.isdir(old): @@ -159,8 +181,6 @@ class Updater: # handle files diff # sync files that exist in both but are different - if self.debug: - self.log.dbg('sync files that exist in both but are different') fdiff = diff.diff_files fdiff.extend(diff.funny_files) fdiff.extend(diff.common_funny) @@ -175,8 +195,6 @@ class Updater: self._handle_file(fleft, fright, compare=False) # copy files that don't exist in dotdrop - if self.debug: - self.log.dbg('copy files not existing in dotdrop') for toadd in diff.left_only: exist = os.path.join(left, toadd) if os.path.isdir(exist): @@ -191,8 +209,6 @@ class Updater: shutil.copyfile(exist, new) # remove files that don't exist in deployed version - if self.debug: - self.log.dbg('remove files that do not exist in deployed version') for toremove in diff.right_only: new = os.path.join(right, toremove) if not os.path.exists(new): diff --git a/tests-ng/dir-import-update.sh b/tests-ng/dir-import-update.sh index 16bff5e..2e5d62b 100755 --- a/tests-ng/dir-import-update.sh +++ b/tests-ng/dir-import-update.sh @@ -47,6 +47,7 @@ echo "RUNNING $(basename $BASH_SOURCE)" # dotdrop directory basedir=`mktemp -d` +dotfiles="${basedir}/dotfiles" echo "dotdrop dir: ${basedir}" # the dotfile tmpd=`mktemp -d` @@ -63,7 +64,7 @@ cd ${ddpath} | ${bin} import -c ${cfg} ${tmpd} echo "changed" > ${token} # update -cd ${ddpath} | ${bin} update -f -c ${cfg} ${tmpd} +cd ${ddpath} | ${bin} update -f -c ${cfg} ${tmpd} --verbose grep 'changed' ${token} >/dev/null 2>&1 diff --git a/tests-ng/update-with-key.sh b/tests-ng/update-with-key.sh new file mode 100755 index 0000000..8d67061 --- /dev/null +++ b/tests-ng/update-with-key.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2017, deadc0de6 +# +# test updates with key +# 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 "RUNNING $(basename $BASH_SOURCE)" + +################################################################ +# this is the test +################################################################ + +# dotdrop directory +basedir=`mktemp -d` +echo "[+] dotdrop dir: ${basedir}" +echo "[+] dotpath dir: ${basedir}/dotfiles" + +# the dotfile to be imported +tmpd=`mktemp -d` + +# originally imported directory +echo 'unique' > ${tmpd}/uniquefile +uniquefile_key="f_uniquefile" +echo 'unique2' > ${tmpd}/uniquefile2 +uniquefile2_key="f_uniquefile2" +mkdir ${tmpd}/dir1 +touch ${tmpd}/dir1/dir1f1 +mkdir ${tmpd}/dir1/dir1dir1 +dir1_key="d_dir1" + +# create the config file +cfg="${basedir}/config.yaml" +create_conf ${cfg} # sets token + +# import dir1 +echo "[+] import" +cd ${ddpath} | ${bin} import -c ${cfg} ${tmpd}/dir1 +cd ${ddpath} | ${bin} import -c ${cfg} ${tmpd}/uniquefile +cd ${ddpath} | ${bin} import -c ${cfg} ${tmpd}/uniquefile2 + +# make some modification +echo "[+] modify" +echo 'changed' > ${tmpd}/uniquefile +echo 'changed' > ${tmpd}/uniquefile2 +echo 'new' > ${tmpd}/dir1/dir1dir1/new + +# update by key +echo "[+] updating single key" +cd ${ddpath} | ${bin} update -c ${cfg} -k -f --verbose ${uniquefile_key} + +# ensure changes applied correctly (only to uniquefile) +diff ${tmpd}/uniquefile ${basedir}/dotfiles/${tmpd}/uniquefile # should be same +set +e +diff ${tmpd}/uniquefile2 ${basedir}/dotfiles/${tmpd}/uniquefile2 # should be different +[ "${?}" != "1" ] && exit 1 +set -e + +# update all keys +echo "[+] updating all keys" +cd ${ddpath} | ${bin} update -c ${cfg} -k -f --verbose + +# ensure all changes applied +diff ${tmpd} ${basedir}/dotfiles/${tmpd} + +## CLEANING +rm -rf ${basedir} ${tmpd} + +echo "OK" +exit 0 diff --git a/tests/test_update.py b/tests/test_update.py index 9d0133e..74f26ee 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -51,6 +51,10 @@ class TestUpdate(unittest.TestCase): self.assertTrue(os.path.exists(d1)) self.addCleanup(clean, d1) + d2, c2 = create_random_file(fold_config) + self.assertTrue(os.path.exists(d2)) + self.addCleanup(clean, d2) + # create the directory to test dpath = os.path.join(fold_config, get_string(5)) dir1 = create_dir(dpath) @@ -65,7 +69,7 @@ class TestUpdate(unittest.TestCase): create=self.CONFIG_CREATE) self.assertTrue(os.path.exists(confpath)) conf, opts = load_config(confpath, profile) - dfiles = [d1, dir1] + dfiles = [d1, dir1, d2] # import the files cmd_importer(opts, conf, dfiles) @@ -94,6 +98,25 @@ class TestUpdate(unittest.TestCase): newcontent = open(dirf1, 'r').read() self.assertTrue(newcontent == 'newcontent') + edit_content(d2, 'newcontentbykey') + + # update it by key + dfiles = conf.get_dotfiles(profile) + d2key = '' + for ds in dfiles: + t = os.path.expanduser(ds.dst) + if t == d2: + d2key = ds.key + break + self.assertTrue(d2key != '') + opts['safe'] = False + opts['debug'] = True + cmd_update(opts, conf, [d2key], iskey=True) + + # test content + newcontent = open(d2, 'r').read() + self.assertTrue(newcontent == 'newcontentbykey') + def main(): unittest.main()