mirror of
https://github.com/deadc0de6/dotdrop.git
synced 2026-02-04 19:09:44 +00:00
Merge branch 'reworkupdate'
This commit is contained in:
@@ -15,6 +15,7 @@ from dotdrop.version import __version__ as VERSION
|
||||
from dotdrop.logger import Logger
|
||||
from dotdrop.templategen import Templategen
|
||||
from dotdrop.installer import Installer
|
||||
from dotdrop.updater import Updater
|
||||
from dotdrop.dotfile import Dotfile
|
||||
from dotdrop.config import Cfg
|
||||
from dotdrop.utils import *
|
||||
@@ -39,7 +40,7 @@ Usage:
|
||||
dotdrop import [-ldVb] [-c <path>] [-p <profile>] <paths>...
|
||||
dotdrop compare [-Vb] [-c <path>] [-p <profile>]
|
||||
[-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 list [-Vb] [-c <path>]
|
||||
dotdrop --help
|
||||
@@ -212,52 +213,12 @@ def compare(opts, conf, tmp, focus=None):
|
||||
return ret
|
||||
|
||||
|
||||
def update(opts, conf, path):
|
||||
"""update the dotfile from path"""
|
||||
if not os.path.lexists(path):
|
||||
LOG.err('\"{}\" does not exist!'.format(path))
|
||||
return False
|
||||
home = os.path.expanduser(TILD)
|
||||
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 update(opts, conf, paths):
|
||||
"""update the dotfile(s) from path(s)"""
|
||||
updater = Updater(conf, opts['dotpath'], opts['dry'],
|
||||
opts['safe'], opts['debug'])
|
||||
for path in paths:
|
||||
updater.update(path, opts['profile'])
|
||||
|
||||
|
||||
def importer(opts, conf, paths):
|
||||
@@ -396,7 +357,7 @@ def main():
|
||||
|
||||
elif args['update']:
|
||||
# update a dotfile
|
||||
update(opts, conf, args['<path>'])
|
||||
update(opts, conf, args['<paths>'])
|
||||
|
||||
except KeyboardInterrupt:
|
||||
LOG.err('interrupted')
|
||||
|
||||
219
dotdrop/updater.py
Normal file
219
dotdrop/updater.py
Normal 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
129
tests-ng/update.sh
Executable 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
|
||||
@@ -202,7 +202,7 @@ class TestImport(unittest.TestCase):
|
||||
with open(dotfile1, 'w') as f:
|
||||
f.write('edited')
|
||||
opts['safe'] = False
|
||||
update(opts, conf, dotfile1)
|
||||
update(opts, conf, [dotfile1])
|
||||
c2 = open(indt1, 'r').read()
|
||||
self.assertTrue(editcontent == c2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user