1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-04 18:34:48 +00:00
Files
dotdrop/dotdrop/dotdrop.py
2019-01-20 15:02:09 -04:00

520 lines
18 KiB
Python

"""
author: deadc0de6 (https://github.com/deadc0de6)
Copyright (c) 2017, deadc0de6
entry point
"""
import os
import sys
import socket
from docopt import docopt
# local imports
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.comparator import Comparator
from dotdrop.dotfile import Dotfile
from dotdrop.config import Cfg
from dotdrop.utils import get_tmpdir, remove, strip_home, run
from dotdrop.linktypes import LinkTypes
LOG = Logger()
ENV_PROFILE = 'DOTDROP_PROFILE'
ENV_NOBANNER = 'DOTDROP_NOBANNER'
PROFILE = socket.gethostname()
if ENV_PROFILE in os.environ:
PROFILE = os.environ[ENV_PROFILE]
TRANS_SUFFIX = 'trans'
BANNER = """ _ _ _
__| | ___ | |_ __| |_ __ ___ _ __
/ _` |/ _ \| __/ _` | '__/ _ \| '_ |
\__,_|\___/ \__\__,_|_| \___/| .__/ v{}
|_|""".format(VERSION)
USAGE = """
{}
Usage:
dotdrop install [-tfndVbD] [-c <path>] [-p <profile>] [<key>...]
dotdrop import [-ldVb] [-c <path>] [-p <profile>] <path>...
dotdrop compare [-Vb] [-c <path>] [-p <profile>]
[-o <opts>] [-C <file>...] [-i <pattern>...]
dotdrop update [-fdVbk] [-c <path>] [-p <profile>] [<path>...]
dotdrop listfiles [-VTb] [-c <path>] [-p <profile>]
dotdrop detail [-Vb] [-c <path>] [-p <profile>] [<key>...]
dotdrop list [-Vb] [-c <path>]
dotdrop --help
dotdrop --version
Options:
-p --profile=<profile> Specify the profile to use [default: {}].
-c --cfg=<path> Path to the config [default: config.yaml].
-C --file=<path> Path of dotfile to compare.
-i --ignore=<pattern> Pattern to ignore when diffing.
-o --dopts=<opts> Diff options [default: ].
-n --nodiff Do not diff when installing.
-t --temp Install to a temporary directory for review.
-T --template Only template dotfiles.
-D --showdiff Show a diff before overwriting.
-l --link Import and link.
-f --force Do not warn if exists.
-k --key Treat <path> as a dotfile key.
-V --verbose Be verbose.
-d --dry Dry run.
-b --no-banner Do not display the banner.
-v --version Show version.
-h --help Show this screen.
""".format(BANNER, PROFILE)
###########################################################
# entry point
###########################################################
def cmd_install(opts, conf, temporary=False, keys=[]):
"""install dotfiles for this profile"""
dotfiles = conf.get_dotfiles(opts['profile'])
if keys:
# filtered dotfiles to install
dotfiles = [d for d in dotfiles if d.key in set(keys)]
if not dotfiles:
msg = 'no dotfile to install for this profile (\"{}\")'
LOG.warn(msg.format(opts['profile']))
return False
t = Templategen(profile=opts['profile'], base=opts['dotpath'],
variables=opts['variables'], debug=opts['debug'])
tmpdir = None
if temporary:
tmpdir = get_tmpdir()
inst = Installer(create=opts['create'], backup=opts['backup'],
dry=opts['dry'], safe=opts['safe'],
base=opts['dotpath'], workdir=opts['workdir'],
diff=opts['installdiff'], debug=opts['debug'],
totemp=tmpdir, showdiff=opts['showdiff'])
installed = []
for dotfile in dotfiles:
preactions = []
if dotfile.actions and Cfg.key_actions_pre in dotfile.actions:
for action in dotfile.actions[Cfg.key_actions_pre]:
preactions.append(action)
if opts['debug']:
LOG.dbg('installing {}'.format(dotfile))
if hasattr(dotfile, 'link') and dotfile.link == LinkTypes.PARENTS:
r = inst.link(t, dotfile.src, dotfile.dst, actions=preactions)
elif hasattr(dotfile, 'link') and dotfile.link == LinkTypes.CHILDREN:
r = inst.linkall(t, dotfile.src, dotfile.dst, actions=preactions)
else:
src = dotfile.src
tmp = None
if dotfile.trans_r:
tmp = apply_trans(opts, dotfile)
if not tmp:
continue
src = tmp
r = inst.install(t, src, dotfile.dst, actions=preactions,
noempty=dotfile.noempty)
if tmp:
tmp = os.path.join(opts['dotpath'], tmp)
if os.path.exists(tmp):
remove(tmp)
if len(r) > 0:
if Cfg.key_actions_post in dotfile.actions:
actions = dotfile.actions[Cfg.key_actions_post]
# execute action
for action in actions:
if opts['dry']:
LOG.dry('would execute action: {}'.format(action))
else:
if opts['debug']:
LOG.dbg('executing post action {}'.format(action))
action.execute()
installed.extend(r)
if temporary:
LOG.log('\nInstalled to tmp {}.'.format(tmpdir))
LOG.log('\n{} dotfile(s) installed.'.format(len(installed)))
return True
def cmd_compare(opts, conf, tmp, focus=[], ignore=[]):
"""compare dotfiles and return True if all identical"""
dotfiles = conf.get_dotfiles(opts['profile'])
if dotfiles == []:
msg = 'no dotfile defined for this profile (\"{}\")'
LOG.warn(msg.format(opts['profile']))
return True
# compare only specific files
same = True
selected = dotfiles
if focus:
selected = _select(focus, dotfiles)
if len(selected) < 1:
return False
t = Templategen(profile=opts['profile'], base=opts['dotpath'],
variables=opts['variables'], debug=opts['debug'])
inst = Installer(create=opts['create'], backup=opts['backup'],
dry=opts['dry'], base=opts['dotpath'],
workdir=opts['workdir'], debug=opts['debug'])
comp = Comparator(diffopts=opts['dopts'], debug=opts['debug'])
for dotfile in selected:
if opts['debug']:
LOG.dbg('comparing {}'.format(dotfile))
src = dotfile.src
if not os.path.lexists(os.path.expanduser(dotfile.dst)):
LOG.emph('\"{}\" does not exist on local\n'.format(dotfile.dst))
tmpsrc = None
if dotfile.trans_r:
# apply transformation
tmpsrc = apply_trans(opts, dotfile)
if not tmpsrc:
# could not apply trans
continue
src = tmpsrc
# install dotfile to temporary dir
ret, insttmp = inst.install_to_temp(t, tmp, src, dotfile.dst)
if not ret:
# failed to install to tmp
continue
ignores = list(set(ignore + dotfile.cmpignore))
diff = comp.compare(insttmp, dotfile.dst, ignore=ignores)
if tmpsrc:
# clean tmp transformed dotfile if any
tmpsrc = os.path.join(opts['dotpath'], tmpsrc)
if os.path.exists(tmpsrc):
remove(tmpsrc)
if diff == '':
if opts['debug']:
LOG.dbg('diffing \"{}\" VS \"{}\"'.format(dotfile.key,
dotfile.dst))
LOG.dbg('same file')
else:
LOG.log('diffing \"{}\" VS \"{}\"'.format(dotfile.key,
dotfile.dst))
LOG.emph(diff)
same = False
return same
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'], 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
cnt = 0
for path in paths:
if opts['debug']:
LOG.dbg('trying to import {}'.format(path))
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)
src = strip_home(dst)
strip = '.' + os.sep
if opts['keepdot']:
strip = os.sep
src = src.lstrip(strip)
# create a new dotfile
dotfile = Dotfile('', dst, src)
linktype = LinkTypes.NOLINK
if opts['link'] or opts['link_by_default']:
linktype = LinkTypes.PARENTS
elif opts['link_children']:
linktype = LinkTypes.CHILDREN
if opts['debug']:
LOG.dbg('new dotfile: {}'.format(dotfile))
# prepare hierarchy for dotfile
srcf = os.path.join(opts['dotpath'], src)
if not os.path.exists(srcf):
cmd = ['mkdir', '-p', '{}'.format(os.path.dirname(srcf))]
if opts['dry']:
LOG.dry('would run: {}'.format(' '.join(cmd)))
else:
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']:
LOG.dry('would run: {}'.format(' '.join(cmd)))
if linktype == LinkTypes.PARENTS:
LOG.dry('would symlink {} to {}'.format(srcf, dst))
else:
r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True)
if not r:
LOG.err('importing \"{}\" failed!'.format(path))
ret = False
continue
if linktype == LinkTypes.PARENTS:
remove(dst)
os.symlink(srcf, dst)
retconf, dotfile = conf.new(dotfile, opts['profile'],
link=linktype, debug=opts['debug'])
if retconf:
LOG.sub('\"{}\" imported'.format(path))
cnt += 1
else:
LOG.warn('\"{}\" ignored'.format(path))
if opts['dry']:
LOG.dry('new config file would be:')
LOG.raw(conf.dump())
else:
conf.save()
LOG.log('\n{} file(s) imported.'.format(cnt))
return ret
def cmd_list_profiles(conf):
"""list all profiles"""
LOG.log('Available profile(s):')
for p in conf.get_profiles():
LOG.sub(p)
LOG.log('')
def cmd_list_files(opts, conf, templateonly=False):
"""list all dotfiles for a specific profile"""
if not opts['profile'] in conf.get_profiles():
LOG.warn('unknown profile \"{}\"'.format(opts['profile']))
return
what = 'Dotfile(s)'
if templateonly:
what = 'Template(s)'
LOG.emph('{} for profile \"{}\"\n'.format(what, opts['profile']))
for dotfile in conf.get_dotfiles(opts['profile']):
if templateonly:
src = os.path.join(opts['dotpath'], dotfile.src)
if not Templategen.is_template(src):
continue
LOG.log('{} (src: \"{}\", link: {})'.format(dotfile.key, dotfile.src,
dotfile.link))
LOG.sub('{}'.format(dotfile.dst))
LOG.log('')
def cmd_detail(opts, conf, keys=None):
"""list details on all files for all dotfile entries"""
if not opts['profile'] in conf.get_profiles():
LOG.warn('unknown profile \"{}\"'.format(opts['profile']))
return
dotfiles = conf.get_dotfiles(opts['profile'])
if keys:
# filtered dotfiles to install
dotfiles = [d for d in dotfiles if d.key in set(keys)]
LOG.emph('dotfiles details for profile \"{}\":\n'.format(opts['profile']))
for d in dotfiles:
_detail(opts['dotpath'], d)
LOG.log('')
###########################################################
# helpers
###########################################################
def _detail(dotpath, dotfile):
"""print details on all files under a dotfile entry"""
LOG.log('{} (dst: \"{}\", link: {})'.format(dotfile.key, dotfile.dst,
dotfile.link))
path = os.path.join(dotpath, os.path.expanduser(dotfile.src))
if not os.path.isdir(path):
template = 'no'
if Templategen.is_template(path):
template = 'yes'
LOG.sub('{} (template:{})'.format(path, template))
else:
for root, dir, files in os.walk(path):
for f in files:
p = os.path.join(root, f)
template = 'no'
if Templategen.is_template(p):
template = 'yes'
LOG.sub('{} (template:{})'.format(p, template))
def _header():
"""print the header"""
LOG.log(BANNER)
LOG.log('')
def _select(selections, dotfiles):
selected = []
for selection in selections:
df = next(
(x for x in dotfiles
if os.path.expanduser(x.dst) == os.path.expanduser(selection)),
None
)
if df:
selected.append(df)
else:
LOG.err('no dotfile matches \"{}\"'.format(selection))
return selected
def apply_trans(opts, dotfile):
"""apply the read transformation to the dotfile
return None if fails and new source if succeed"""
src = dotfile.src
new_src = '{}.{}'.format(src, TRANS_SUFFIX)
trans = dotfile.trans_r
if opts['debug']:
LOG.dbg('executing transformation {}'.format(trans))
s = os.path.join(opts['dotpath'], src)
temp = os.path.join(opts['dotpath'], new_src)
if not trans.transform(s, temp):
msg = 'transformation \"{}\" failed for {}'
LOG.err(msg.format(trans.key, dotfile.key))
if new_src and os.path.exists(new_src):
remove(new_src)
return None
return new_src
###########################################################
# main
###########################################################
def main():
"""entry point"""
ret = True
args = docopt(USAGE, version=VERSION)
try:
conf = Cfg(os.path.expanduser(args['--cfg']))
except ValueError as e:
LOG.err('Config format error: {}'.format(str(e)))
return False
opts = conf.get_settings()
opts['dry'] = args['--dry']
opts['profile'] = args['--profile']
opts['safe'] = not args['--force']
opts['installdiff'] = not args['--nodiff']
opts['link'] = args['--link']
opts['debug'] = args['--verbose']
opts['variables'] = conf.get_variables()
opts['showdiff'] = opts['showdiff'] or args['--showdiff']
if opts['debug']:
LOG.dbg('config file: {}'.format(args['--cfg']))
LOG.dbg('options:\n{}'.format(opts))
LOG.dbg('configs:\n{}'.format(conf.dump()))
# resolve dynamic paths
conf.eval_dotfiles(opts['profile'], debug=opts['debug'])
if ENV_NOBANNER not in os.environ \
and opts['banner'] \
and not args['--no-banner']:
_header()
try:
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['<key>'])
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'],
ignore=args['--ignore'])
# clean tmp directory
remove(tmp)
elif args['import']:
# import dotfile(s)
if opts['debug']:
LOG.dbg('running cmd: import')
ret = cmd_importer(opts, conf, args['<path>'])
elif args['update']:
# update a dotfile
if opts['debug']:
LOG.dbg('running cmd: update')
iskey = args['--key']
ret = cmd_update(opts, conf, args['<path>'], iskey=iskey)
elif args['detail']:
# detail files
if opts['debug']:
LOG.dbg('running cmd: update')
cmd_detail(opts, conf, keys=args['<key>'])
except KeyboardInterrupt:
LOG.err('interrupted')
ret = False
if opts['debug']:
LOG.dbg('configs:\n{}'.format(conf.dump()))
return ret
if __name__ == '__main__':
if main():
sys.exit(0)
sys.exit(1)