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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
*.pyc
|
||||
.coverage
|
||||
.idea/
|
||||
.vscode/
|
||||
.atom/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -68,6 +68,7 @@ def load_config(confpath, profile):
|
||||
opts['profile'] = profile
|
||||
opts['safe'] = True
|
||||
opts['installdiff'] = True
|
||||
opts['link'] = False
|
||||
return conf, opts
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user