1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-05 21:23:02 +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
.coverage
.idea/
.vscode/
.atom/

View File

@@ -177,11 +177,12 @@ the following entries:
* `dotpath`: path to the folder containing the dotfiles to be managed
by dotdrop (absolute path or relative to the config file location)
* **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>:
dst: <where-this-file-is-deployed>
src: <filename-within-the-dotpath>
link: <true|false> # Optional
```
* **profiles** entry: a list of profiles with a sublist

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,35 @@ class Installer:
return self._handle_dir(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):
'''Install a file using templater for "profile"'''
content = templater.generate(src, profile)
@@ -47,7 +76,7 @@ class Installer:
self.log.sub('ignoring \"%s\", same content' % (dst))
return []
if ret == 0:
if not self.quiet:
if not self.quiet and not self.dry:
self.log.sub('copied %s to %s' % (src, dst))
return [(src, dst)]
return []

View File

@@ -6,7 +6,10 @@ utilities
import subprocess
import tempfile
import os
from logger import Logger
from shutil import rmtree
LOG = Logger()
@@ -29,3 +32,15 @@ def diff(src, dst, log=False, raw=True):
def get_tmpdir():
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['safe'] = True
opts['installdiff'] = True
opts['link'] = False
return conf, opts

View File

@@ -42,6 +42,7 @@ exec bspwm
f.write(' %s:\n' % (d.key))
f.write(' dst: %s\n' % (d.dst))
f.write(' src: %s\n' % (d.src))
f.write(' link: %s\n' % str(d.link).lower())
f.write('profiles:\n')
f.write(' %s:\n' % (profile))
for d in dotfiles:
@@ -78,10 +79,15 @@ exec bspwm
with open(dst4, 'w') as f:
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
profile = get_string(5)
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)
self.assertTrue(conf is not None)
@@ -94,6 +100,11 @@ exec bspwm
self.assertTrue(os.path.exists(dst1))
self.assertTrue(os.path.exists(dst2))
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
b = dst4 + Installer.BACKUP_SUFFIX