1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-10 06:54:17 +00:00

Merge branch 'reworkupdate'

This commit is contained in:
deadc0de6
2018-07-21 13:45:18 +02:00
4 changed files with 358 additions and 49 deletions

View File

@@ -15,6 +15,7 @@ from dotdrop.version import __version__ as VERSION
from dotdrop.logger import Logger from dotdrop.logger import Logger
from dotdrop.templategen import Templategen from dotdrop.templategen import Templategen
from dotdrop.installer import Installer from dotdrop.installer import Installer
from dotdrop.updater import Updater
from dotdrop.dotfile import Dotfile from dotdrop.dotfile import Dotfile
from dotdrop.config import Cfg from dotdrop.config import Cfg
from dotdrop.utils import * from dotdrop.utils import *
@@ -39,7 +40,7 @@ Usage:
dotdrop import [-ldVb] [-c <path>] [-p <profile>] <paths>... dotdrop import [-ldVb] [-c <path>] [-p <profile>] <paths>...
dotdrop compare [-Vb] [-c <path>] [-p <profile>] dotdrop compare [-Vb] [-c <path>] [-p <profile>]
[-o <opts>] [--files=<files>] [-o <opts>] [--files=<files>]
dotdrop update [-fdVb] [-c <path>] <path> dotdrop update [-fdVb] [-c <path>] [-p <profile>] <paths>...
dotdrop listfiles [-Vb] [-c <path>] [-p <profile>] dotdrop listfiles [-Vb] [-c <path>] [-p <profile>]
dotdrop list [-Vb] [-c <path>] dotdrop list [-Vb] [-c <path>]
dotdrop --help dotdrop --help
@@ -212,52 +213,12 @@ def compare(opts, conf, tmp, focus=None):
return ret return ret
def update(opts, conf, path): def update(opts, conf, paths):
"""update the dotfile from path""" """update the dotfile(s) from path(s)"""
if not os.path.lexists(path): updater = Updater(conf, opts['dotpath'], opts['dry'],
LOG.err('\"{}\" does not exist!'.format(path)) opts['safe'], opts['debug'])
return False for path in paths:
home = os.path.expanduser(TILD) updater.update(path, opts['profile'])
path = os.path.expanduser(path)
path = os.path.expandvars(path)
# normalize the path
if path.startswith(home):
path = path.lstrip(home)
path = os.path.join(TILD, path)
dotfiles = conf.get_dotfiles(opts['profile'])
subs = [d for d in dotfiles if d.dst == path]
if not subs:
LOG.err('\"{}\" is not managed!'.format(path))
return False
if len(subs) > 1:
found = ','.join([d.src for d in dotfiles])
LOG.err('multiple dotfiles found: {}'.format(found))
return False
dotfile = subs[0]
src = os.path.join(conf.abs_dotpath(opts['dotpath']), dotfile.src)
if os.path.isfile(src) and \
Templategen.get_marker() in open(src, 'r').read():
LOG.warn('\"{}\" uses template, please update manually'.format(src))
return False
# Handle directory update
src_clean = src
if os.path.isdir(src):
src_clean = os.path.join(src, '..')
if samefile(src_clean, path):
# symlink loop
Log.err('dotfile points to itself: {}'.format(path))
return False
cmd = ['cp', '-R', '-L', os.path.expanduser(path), src_clean]
if opts['dry']:
LOG.dry('would run: {}'.format(' '.join(cmd)))
else:
msg = 'Overwrite \"{}\" with \"{}\"?'.format(src, path)
if opts['safe'] and not LOG.ask(msg):
return False
else:
run(cmd, raw=False, debug=opts['debug'])
LOG.log('\"{}\" updated from \"{}\".'.format(src, path))
return True
def importer(opts, conf, paths): def importer(opts, conf, paths):
@@ -396,7 +357,7 @@ def main():
elif args['update']: elif args['update']:
# update a dotfile # update a dotfile
update(opts, conf, args['<path>']) update(opts, conf, args['<paths>'])
except KeyboardInterrupt: except KeyboardInterrupt:
LOG.err('interrupted') LOG.err('interrupted')

219
dotdrop/updater.py Normal file
View File

@@ -0,0 +1,219 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
Copyright (c) 2017, deadc0de6
handle the update of dotfiles
"""
import os
import shutil
import filecmp
# local imports
from dotdrop.logger import Logger
from dotdrop.templategen import Templategen
import dotdrop.utils as utils
TILD = '~'
class Updater:
BACKUP_SUFFIX = '.dotdropbak'
def __init__(self, conf, dotpath, dry, safe, debug):
self.home = os.path.expanduser(TILD)
self.conf = conf
self.dotpath = dotpath
self.dry = dry
self.safe = safe
self.debug = debug
self.log = Logger()
def _normalize(self, path):
"""normalize the path to match dotfile"""
path = os.path.expanduser(path)
path = os.path.expandvars(path)
# normalize the path
if path.startswith(self.home):
path = path.lstrip(self.home)
path = os.path.join(TILD, path)
return path
def _get_dotfile(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]
if not subs:
self.log.err('\"{}\" is not managed!'.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 update(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(path, profile)
if not dotfile:
return False
if self.debug:
self.log.dbg('updating {} from {}'.format(dotfile, path))
right = os.path.join(self.conf.abs_dotpath(self.dotpath), dotfile.src)
# go through all files and update
if os.path.isdir(path):
return self._handle_dir(left, right)
return self._handle_file(left, right)
def _is_template(self, path):
if Templategen.get_marker() not in open(path, 'r').read():
return False
self.log.warn('{} uses template, update manually'.format(right))
return True
def _handle_file(self, left, right, compare=True):
"""sync left (deployed file) and right (dotdrop dotfile)"""
if self.debug:
self.log.dbg('update for file {} and {}'.format(left, right))
if self._is_template(right):
return False
if compare and filecmp.cmp(left, right, shallow=True):
# no difference
if self.debug:
self.log.dbg('identical files: {} and {}'.format(left, right))
return True
if not self._overwrite(left, right):
return False
try:
if self.dry:
self.log.dry('would cp {} {}'.format(left, right))
else:
if self.debug:
self.log.dbg('cp {} {}'.format(left, right))
shutil.copyfile(left, right)
except IOError as e:
self.log.warn('{} update failed, do manually: {}'.format(left, e))
return False
return True
def _handle_dir(self, left, right):
"""sync left (deployed dir) and right (dotdrop dir)"""
if self.debug:
self.log.dbg('handle update for dir {} to {}'.format(left, right))
# find the difference
diff = filecmp.dircmp(left, right, ignore=None)
# handle directories diff
# 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):
# ignore files for now
continue
# match to dotdrop dotpath
new = os.path.join(right, toadd)
if self.dry:
self.log.dry('would mkdir -p {}'.format(new))
continue
if self.debug:
self.log.dbg('mkdir -p {}'.format(new))
self._create_dirs(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:
new = os.path.join(right, toremove)
if self.dry:
self.log.dry('would rm -r {}'.format(new))
continue
if self.debug:
self.log.dbg('rm -r {}'.format(new))
if not self._confirm_rm_r(new):
continue
utils.remove(new)
# 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)
for f in fdiff:
fleft = os.path.join(left, f)
fright = os.path.join(right, f)
if self.dry:
self.log.dry('would cp {} {}'.format(fleft, fright))
continue
if self.debug:
self.log.dbg('cp {} {}'.format(fleft, fright))
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):
# ignore dirs, done above
continue
new = os.path.join(right, toadd)
if self.dry:
self.log.dry('would cp {} {}'.format(exist, new))
continue
if self.debug:
self.log.dbg('cp {} {}'.format(exist, new))
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):
continue
if os.path.isdir(new):
# ignore dirs, done above
continue
if self.dry:
self.log.dry('would rm {}'.format(new))
continue
if self.debug:
self.log.dbg('rm {}'.format(new))
utils.remove(new)
return True
def _create_dirs(self, directory):
"""mkdir -p <directory>"""
if os.path.exists(directory):
return True
if self.dry:
self.log.dry('would mkdir -p {}'.format(directory))
return True
if self.debug:
self.log.dbg('mkdir -p {}'.format(directory))
os.makedirs(directory)
return os.path.exists(directory)
def _overwrite(self, src, dst):
"""ask for overwritting"""
msg = 'Overwrite \"{}\" with \"{}\"?'.format(dst, src)
if self.safe and not self.log.ask(msg):
return False
return True
def _confirm_rm_r(self, directory):
"""ask for rm -r directory"""
msg = 'Recursively remove \"{}\"?'.format(directory)
if self.safe and not self.log.ask(msg):
return False
return True

129
tests-ng/update.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# author: deadc0de6 (https://github.com/deadc0de6)
# Copyright (c) 2017, deadc0de6
#
# test updates
# 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`
# single file
echo 'unique' > ${tmpd}/uniquefile
# hierarchy from https://pymotw.com/2/filecmp/
# create the hierarchy
# for dir1 (originally imported directory))))
mkdir ${tmpd}/dir1
touch ${tmpd}/dir1/file_only_in_dir1
mkdir -p ${tmpd}/dir1/dir_only_in_dir1
mkdir -p ${tmpd}/dir1/common_dir
echo 'this file is the same' > ${tmpd}/dir1/common_file
echo 'in dir1' > ${tmpd}/dir1/not_the_same
echo 'This is a file in dir1' > ${tmpd}/dir1/file_in_dir1
mkdir -p ${tmpd}/dir1/sub/sub2
mkdir -p ${tmpd}/dir1/notindir2/notindir2
echo 'first' > ${tmpd}/dir1/sub/sub2/different
#tree ${tmpd}/dir1
# create the hierarchy
# for dir2 (modified original for update)
mkdir ${tmpd}/dir2
touch ${tmpd}/dir2/file_only_in_dir2
mkdir -p ${tmpd}/dir2/dir_only_in_dir2
mkdir -p ${tmpd}/dir2/common_dir
echo 'this file is the same' > ${tmpd}/dir2/common_file
echo 'in dir2' > ${tmpd}/dir2/not_the_same
mkdir -p ${tmpd}/dir2/file_in_dir1
mkdir -p ${tmpd}/dir2/sub/sub2
echo 'modified' > ${tmpd}/dir2/sub/sub2/different
mkdir -p ${tmpd}/dir2/new/new2
#tree ${tmpd}/dir2
# 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
# let's see the dotpath
#tree ${basedir}/dotfiles
# change dir1 to dir2 in deployed
echo "[+] change dir"
rm -rf ${tmpd}/dir1
mv ${tmpd}/dir2 ${tmpd}/dir1
#tree ${tmpd}/dir1
# change unique file
echo 'changed' > ${tmpd}/uniquefile
# compare
#echo "[+] comparing"
#cd ${ddpath} | ${bin} compare -c ${cfg}
# update
echo "[+] updating"
cd ${ddpath} | ${bin} update -c ${cfg} -f --verbose ${tmpd}/uniquefile ${tmpd}/dir1
# manually update
rm ${basedir}/dotfiles/${tmpd}/dir1/file_in_dir1
mkdir -p ${basedir}/dotfiles/${tmpd}/dir1/file_in_dir1
# ensure changes applied correctly
diff ${tmpd}/dir1 ${basedir}/dotfiles/${tmpd}/dir1
diff ${tmpd}/uniquefile ${basedir}/dotfiles/${tmpd}/uniquefile
## CLEANING
rm -rf ${basedir} ${tmpd}
echo "OK"
exit 0

View File

@@ -202,7 +202,7 @@ class TestImport(unittest.TestCase):
with open(dotfile1, 'w') as f: with open(dotfile1, 'w') as f:
f.write('edited') f.write('edited')
opts['safe'] = False opts['safe'] = False
update(opts, conf, dotfile1) update(opts, conf, [dotfile1])
c2 = open(indt1, 'r').read() c2 = open(indt1, 'r').read()
self.assertTrue(editcontent == c2) self.assertTrue(editcontent == c2)