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

Merge pull request #3 from moyiz/master

Support for symlinking dotfiles
This commit is contained in:
deadc0de
2017-05-06 09:51:09 +02:00
committed by GitHub
9 changed files with 92 additions and 17 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
*.pyc *.pyc
.coverage .coverage
.idea/
.vscode/
.atom/

View File

@@ -177,11 +177,12 @@ the following entries:
* `dotpath`: path to the folder containing the dotfiles to be managed * `dotpath`: path to the folder containing the dotfiles to be managed
by dotdrop (absolute path or relative to the config file location) by dotdrop (absolute path or relative to the config file location)
* **dotfiles** entry: a list of dotfiles in the form * **dotfiles** entry: a list of dotfiles in the form
* When `link` is true, dotdrop will create a link instead of copying. Template generation (as in [template](#template)) is not supported when `link` is true.
``` ```
<dotfile-key-name>: <dotfile-key-name>:
dst: <where-this-file-is-deployed> dst: <where-this-file-is-deployed>
src: <filename-within-the-dotpath> src: <filename-within-the-dotpath>
link: <true|false> # Optional
``` ```
* **profiles** entry: a list of profiles with a sublist * **profiles** entry: a list of profiles with a sublist

View File

@@ -18,6 +18,7 @@ class Cfg:
key_dotpath = 'dotpath' key_dotpath = 'dotpath'
key_dotfiles_src = 'src' key_dotfiles_src = 'src'
key_dotfiles_dst = 'dst' key_dotfiles_dst = 'dst'
key_dotfiles_link = 'link'
def __init__(self, cfgpath): def __init__(self, cfgpath):
if not os.path.exists(cfgpath): if not os.path.exists(cfgpath):
@@ -61,7 +62,9 @@ class Cfg:
for k, v in self.content[self.key_dotfiles].items(): for k, v in self.content[self.key_dotfiles].items():
src = v[self.key_dotfiles_src] src = v[self.key_dotfiles_src]
dst = v[self.key_dotfiles_dst] dst = v[self.key_dotfiles_dst]
self.dotfiles[k] = Dotfile(k, dst, src) link = v[self.key_dotfiles_link] if self.key_dotfiles_link \
in v else False
self.dotfiles[k] = Dotfile(k, dst, src, link)
# contains a list of dotfiles defined for each profile # contains a list of dotfiles defined for each profile
for k, v in self.profiles.items(): for k, v in self.profiles.items():
self.prodots[k] = [] self.prodots[k] = []
@@ -84,7 +87,7 @@ class Cfg:
return absconf return absconf
return dotpath return dotpath
def new(self, dotfile, profile): def new(self, dotfile, profile, link=False):
""" import new dotfile """ """ import new dotfile """
dots = self.content[self.key_dotfiles] dots = self.content[self.key_dotfiles]
if dots is None: if dots is None:
@@ -100,6 +103,8 @@ class Cfg:
self.key_dotfiles_dst: dotfile.dst, self.key_dotfiles_dst: dotfile.dst,
self.key_dotfiles_src: dotfile.src self.key_dotfiles_src: dotfile.src
} }
if link:
dots[dotfile.key][self.key_dotfiles_link] = True
profiles = self.profiles profiles = self.profiles
if profile in profiles and profiles[profile] != [self.key_all]: if profile in profiles and profiles[profile] != [self.key_all]:
if self.content[self.key_profiles][profile] is None: if self.content[self.key_profiles][profile] is None:
@@ -115,8 +120,7 @@ class Cfg:
""" returns a list of dotfiles for a specific profile """ """ returns a list of dotfiles for a specific profile """
if profile not in self.prodots: if profile not in self.prodots:
return [] return []
tmp = sorted(self.prodots[profile], key=lambda x: x.key, reverse=True) return sorted(self.prodots[profile], key=lambda x: x.key, reverse=True)
return tmp
def get_profiles(self): def get_profiles(self):
""" returns all defined profiles """ """ returns all defined profiles """

View File

@@ -34,7 +34,8 @@ Usage:
[(-f | --force)] [--nodiff] [--dry] [(-f | --force)] [--nodiff] [--dry]
dotdrop.py compare [--profile=<profile>] [--cfg=<path>] dotdrop.py compare [--profile=<profile>] [--cfg=<path>]
dotdrop.py list [--cfg=<path>] dotdrop.py list [--cfg=<path>]
dotdrop.py import [--cfg=<path>] [--profile=<profile>] [--dry] <paths>... dotdrop.py import [--profile=<profile>] [--cfg=<path>]
[(-l | --link)] [--dry] <paths>...
dotdrop.py (-h | --help) dotdrop.py (-h | --help)
dotdrop.py (-v | --version) dotdrop.py (-v | --version)
@@ -43,6 +44,7 @@ Options:
--cfg=<path> Path to the config [default: %s/config.yaml]. --cfg=<path> Path to the config [default: %s/config.yaml].
--dry Dry run. --dry Dry run.
--nodiff Do not diff when installing [default: False]. --nodiff Do not diff when installing [default: False].
-l --link Import and link [default: False].
-f --force Do not warn if exists [default: False]. -f --force Do not warn if exists [default: False].
-v --version Show version. -v --version Show version.
-h --help Show this screen. -h --help Show this screen.
@@ -66,7 +68,10 @@ def install(opts, conf):
diff=opts['installdiff']) diff=opts['installdiff'])
installed = [] installed = []
for dotfile in dotfiles: for dotfile in dotfiles:
r = inst.install(t, opts['profile'], dotfile.src, dotfile.dst) if hasattr(dotfile, "link") and dotfile.link:
r = inst.link(dotfile.src, dotfile.dst)
else:
r = inst.install(t, opts['profile'], dotfile.src, dotfile.dst)
installed.extend(r) installed.extend(r)
LOG.log('\n%u dotfile(s) installed.' % (len(installed))) LOG.log('\n%u dotfile(s) installed.' % (len(installed)))
return True return True
@@ -98,8 +103,7 @@ def importer(opts, conf, paths):
key = dst.split(os.sep)[-1] key = dst.split(os.sep)[-1]
if key == 'config': if key == 'config':
key = '_'.join(dst.split(os.sep)[-2:]) key = '_'.join(dst.split(os.sep)[-2:])
key = key.lstrip('.') key = key.lstrip('.').lower()
key = key.lower()
if os.path.isdir(dst): if os.path.isdir(dst):
key = 'd_%s' % (key) key = 'd_%s' % (key)
else: else:
@@ -113,17 +117,22 @@ def importer(opts, conf, paths):
if os.path.exists(srcf): if os.path.exists(srcf):
LOG.err('\"%s\" already exists, ignored !' % (srcf)) LOG.err('\"%s\" already exists, ignored !' % (srcf))
continue continue
conf.new(dotfile, opts['profile']) conf.new(dotfile, opts['profile'], opts['link'])
cmd = ['mkdir', '-p', '%s' % (os.path.dirname(srcf))] cmd = ['mkdir', '-p', '%s' % (os.path.dirname(srcf))]
if opts['dry']: if opts['dry']:
LOG.dry('would run: %s' % (' '.join(cmd))) LOG.dry('would run: %s' % (' '.join(cmd)))
else: else:
utils.run(cmd, raw=False, log=False) utils.run(cmd, raw=False, log=False)
cmd = ['cp', '-r', '%s' % (dst), '%s' % (srcf)] if opts['link']:
cmd = ['mv', '%s' % (dst), '%s' % (srcf)]
else:
cmd = ['cp', '-r', '%s' % (dst), '%s' % (srcf)]
if opts['dry']: if opts['dry']:
LOG.dry('would run: %s' % (' '.join(cmd))) LOG.dry('would run: %s' % (' '.join(cmd)))
else: else:
utils.run(cmd, raw=False, log=False) utils.run(cmd, raw=False, log=False)
if opts['link']:
os.symlink(srcf, dst)
LOG.sub('\"%s\" imported' % (path)) LOG.sub('\"%s\" imported' % (path))
cnt += 1 cnt += 1
if opts['dry']: if opts['dry']:
@@ -156,6 +165,7 @@ if __name__ == '__main__':
opts['profile'] = args['--profile'] opts['profile'] = args['--profile']
opts['safe'] = not args['--force'] opts['safe'] = not args['--force']
opts['installdiff'] = not args['--nodiff'] opts['installdiff'] = not args['--nodiff']
opts['link'] = args['--link']
header() header()

View File

@@ -7,15 +7,16 @@ represents a dotfile in dotdrop
class Dotfile: class Dotfile:
def __init__(self, key, dst, src): def __init__(self, key, dst, src, link=False):
# key of dotfile in the config # key of dotfile in the config
self.key = key self.key = key
# where to install this dotfile # where to install this dotfile
self.dst = dst self.dst = dst
# stored dotfile in dotdrop # stored dotfile in dotdrop
self.src = src self.src = src
# should be a link
self.link = link
def __str__(self): def __str__(self):
string = 'key:%s, src: %s, dst: %s' % (self.key, return 'key:%s, src: %s, dst: %s, link: %s' % (self.key, self.src,
self.src, self.dst) self.dst, self.link)
return string

View File

@@ -32,6 +32,35 @@ class Installer:
return self._handle_dir(templater, profile, src, dst) return self._handle_dir(templater, profile, src, dst)
return self._handle_file(templater, profile, src, dst) return self._handle_file(templater, profile, src, dst)
def link(self, src, dst):
'''Sets src as the link target of dst'''
src = os.path.join(self.base, os.path.expanduser(src))
dst = os.path.join(self.base, os.path.expanduser(dst))
if os.path.exists(dst):
if os.path.realpath(dst) == os.path.realpath(src):
self.log.sub('ignoring "%s", link exists' % dst)
return []
if self.dry:
self.log.dry('would remove %s and link it to %s'
% (dst, src))
return []
if self.safe and \
not self.log.ask('Remove "%s" for link creation?' % dst):
self.log.warn('ignoring "%s", link was not created' % dst)
return []
try:
utils.remove(dst)
except OSError:
self.log.err('something went wrong with %s' % src)
return []
if self.dry:
self.log.dry('would link %s to %s' % (dst, src))
return []
os.symlink(src, dst)
self.log.sub('linked %s to %s' % (dst, src))
# Follows original developer's behavior
return [(src, dst)]
def _handle_file(self, templater, profile, src, dst): def _handle_file(self, templater, profile, src, dst):
'''Install a file using templater for "profile"''' '''Install a file using templater for "profile"'''
content = templater.generate(src, profile) content = templater.generate(src, profile)
@@ -47,7 +76,7 @@ class Installer:
self.log.sub('ignoring \"%s\", same content' % (dst)) self.log.sub('ignoring \"%s\", same content' % (dst))
return [] return []
if ret == 0: if ret == 0:
if not self.quiet: if not self.quiet and not self.dry:
self.log.sub('copied %s to %s' % (src, dst)) self.log.sub('copied %s to %s' % (src, dst))
return [(src, dst)] return [(src, dst)]
return [] return []

View File

@@ -6,7 +6,10 @@ utilities
import subprocess import subprocess
import tempfile import tempfile
import os
from logger import Logger from logger import Logger
from shutil import rmtree
LOG = Logger() LOG = Logger()
@@ -29,3 +32,15 @@ def diff(src, dst, log=False, raw=True):
def get_tmpdir(): def get_tmpdir():
return tempfile.mkdtemp(prefix='dotdrop-') return tempfile.mkdtemp(prefix='dotdrop-')
def remove(path):
''' Remove a file / directory / symlink '''
if not os.path.exists(path):
raise OSError("File not found: %s" % path)
if os.path.islink(path) or os.path.isfile(path):
os.unlink(path)
elif os.path.isdir(path):
rmtree(path)
else:
raise OSError("Unsupported file type for deletion: %s" % path)

View File

@@ -68,6 +68,7 @@ def load_config(confpath, profile):
opts['profile'] = profile opts['profile'] = profile
opts['safe'] = True opts['safe'] = True
opts['installdiff'] = True opts['installdiff'] = True
opts['link'] = False
return conf, opts return conf, opts

View File

@@ -42,6 +42,7 @@ exec bspwm
f.write(' %s:\n' % (d.key)) f.write(' %s:\n' % (d.key))
f.write(' dst: %s\n' % (d.dst)) f.write(' dst: %s\n' % (d.dst))
f.write(' src: %s\n' % (d.src)) f.write(' src: %s\n' % (d.src))
f.write(' link: %s\n' % str(d.link).lower())
f.write('profiles:\n') f.write('profiles:\n')
f.write(' %s:\n' % (profile)) f.write(' %s:\n' % (profile))
for d in dotfiles: for d in dotfiles:
@@ -78,10 +79,15 @@ exec bspwm
with open(dst4, 'w') as f: with open(dst4, 'w') as f:
f.write(get_string(16)) f.write(get_string(16))
# to test link
f5, c5 = create_random_file(tmp)
dst5 = os.path.join(dst, get_string(6))
d5 = Dotfile(get_string(6), dst5, os.path.basename(f5), link=True)
# generate the config and stuff # generate the config and stuff
profile = get_string(5) profile = get_string(5)
confpath = os.path.join(tmp, self.CONFIG_NAME) confpath = os.path.join(tmp, self.CONFIG_NAME)
self.fake_config(confpath, [d1, d2, d3, d4], profile, tmp) self.fake_config(confpath, [d1, d2, d3, d4, d5], profile, tmp)
conf = Cfg(confpath) conf = Cfg(confpath)
self.assertTrue(conf is not None) self.assertTrue(conf is not None)
@@ -94,6 +100,11 @@ exec bspwm
self.assertTrue(os.path.exists(dst1)) self.assertTrue(os.path.exists(dst1))
self.assertTrue(os.path.exists(dst2)) self.assertTrue(os.path.exists(dst2))
self.assertTrue(os.path.exists(dst3)) self.assertTrue(os.path.exists(dst3))
self.assertTrue(os.path.exists(dst5))
# check if 'dst5' is a link whose target is 'f5'
self.assertTrue(os.path.islink(dst5))
self.assertTrue(os.path.realpath(dst5) == os.path.realpath(f5))
# make sure backup is there # make sure backup is there
b = dst4 + Installer.BACKUP_SUFFIX b = dst4 + Installer.BACKUP_SUFFIX