From f309ff80f8e76fb5c95184cf7ab06673ea61934f Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 20 Jul 2018 23:35:44 +0200 Subject: [PATCH] reimplement update --- dotdrop/dotdrop.py | 57 ++---------- dotdrop/updater.py | 210 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 dotdrop/updater.py diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index c605db0..10bccaa 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -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 ] [-p ] ... dotdrop compare [-Vb] [-c ] [-p ] [-o ] [--files=] - dotdrop update [-fdVb] [-c ] + dotdrop update [-fdVb] [-c ] [-p ] ... dotdrop listfiles [-Vb] [-c ] [-p ] dotdrop list [-Vb] [-c ] 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['']) + update(opts, conf, args['']) except KeyboardInterrupt: LOG.err('interrupted') diff --git a/dotdrop/updater.py b/dotdrop/updater.py new file mode 100644 index 0000000..0880ff1 --- /dev/null +++ b/dotdrop/updater.py @@ -0,0 +1,210 @@ +""" +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, hide=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)) + 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 """ + 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