summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Jon Bernard <jbernard@tuxion.com> 2011-08-27 10:52:28 -0400
committerGravatar Jon Bernard <jbernard@tuxion.com> 2011-08-27 10:52:28 -0400
commit55ae047d065b4b8ac78050d7c7a1cb32a8139f86 (patch)
treed7dbab13d07360d4ecb927ed93b761e0d806ef92
parentdbad463a5aa1c45daabb2f90d84d1626a0c6e658 (diff)
parent37734b9b880c32a9a8b602be7ea0979951e2c27b (diff)
downloaddotfiles-0.4.0.tar.gz
dotfiles-0.4.0.tar.bz2
dotfiles-0.4.0.zip
Merge branch 'release/0.4.0'v0.4.0
-rw-r--r--README.rst10
-rw-r--r--contrib/dotfilesrc1
-rw-r--r--dotfiles/cli.py44
-rw-r--r--dotfiles/core.py118
-rwxr-xr-x[-rw-r--r--]setup.py13
-rwxr-xr-xtest_dotfiles.py85
6 files changed, 222 insertions, 49 deletions
diff --git a/README.rst b/README.rst
index 132ad2b..5e959fd 100644
--- a/README.rst
+++ b/README.rst
@@ -4,11 +4,8 @@ Dotfile management made easy
``dotfiles`` is a tool to make managing your dotfile symlinks in ``$HOME``
easy, allowing you to keep all your dotfiles in a single directory.
-Hosting is left to you. Yes, I've seen `<http://dotfiles.org>`_ but I don't
-like that model. If you're advanced enough to need dotfile management, then
-you probably already know how you want to host them. Using whatever VCS you
-prefer, or even rsync, you can easily distribute your dotfiles repository
-across multiple hosts.
+Hosting is left to you. Using whatever VCS you prefer, or even rsync, you can
+easily distribute your dotfiles repository across multiple hosts.
Interface
---------
@@ -29,6 +26,9 @@ Interface
Update dotfile symlinks. You can overwrite unmanaged files with ``-f`` or
``--force``.
+``-m, --move``
+ Move dotfiles repository to another location.
+
Installation
------------
diff --git a/contrib/dotfilesrc b/contrib/dotfilesrc
index fe1de3e..f26515e 100644
--- a/contrib/dotfilesrc
+++ b/contrib/dotfilesrc
@@ -8,5 +8,6 @@ ignore = [
externals = {
'.adobe': '/tmp',
'.bzr.log': '/dev/null',
+ '.lastpass': '/tmp',
'.macromedia': '/tmp',
'.uml': '/tmp'}
diff --git a/dotfiles/cli.py b/dotfiles/cli.py
index 7b8ca59..d00788e 100644
--- a/dotfiles/cli.py
+++ b/dotfiles/cli.py
@@ -14,12 +14,8 @@ from optparse import OptionParser, OptionGroup
DEFAULT_REPO = "~/Dotfiles"
-def method_list(object):
- return [method for method in dir(object)
- if callable(getattr(object, method))]
-
-
def parse_args():
+
parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]")
parser.set_defaults(config=os.path.expanduser("~/.dotfilesrc"))
@@ -43,6 +39,9 @@ def parse_args():
parser.add_option("-C", "--config", type="string", dest="config",
help="set configuration file location (default is ~/.dotfilesrc)")
+ parser.add_option("-H", "--home", type="string", dest="home",
+ help="set home directory location (default is ~/)")
+
action_group = OptionGroup(parser, "Actions")
action_group.add_option("-a", "--add", action="store_const", dest="action",
@@ -62,6 +61,10 @@ def parse_args():
action_group.add_option("-s", "--sync", action="store_const",
dest="action", const="sync", help="update dotfile symlinks")
+ action_group.add_option("-m", "--move", action="store_const",
+ dest="action", const="move", help="move dotfiles repository to " \
+ "another location")
+
parser.add_option_group(action_group)
(opts, args) = parser.parse_args()
@@ -94,6 +97,10 @@ def main():
if not opts.repo and parser.get('dotfiles', 'repository'):
opts.repo = os.path.expanduser(parser.get('dotfiles', 'repository'))
+ if opts.action == 'move':
+ # TODO: update the configuration file after the move
+ print 'Remember to update the repository location ' \
+ 'in your configuration file (%s).' % (opts.config)
if not opts.prefix and parser.get('dotfiles', 'prefix'):
opts.prefix = parser.get('dotfiles', 'prefix')
@@ -122,11 +129,28 @@ def main():
print "Error: An action is required. Type 'dotfiles -h' to see detailed usage information."
exit(-1)
- getattr(core.Dotfiles(location=opts.repo,
- prefix=opts.prefix,
- ignore=opts.ignore,
- externals=opts.externals,
- force=opts.force), opts.action)(files=args)
+ dotfiles = core.Dotfiles(home='~/', repo=opts.repo, prefix=opts.prefix,
+ ignore=opts.ignore, externals=opts.externals)
+
+ if opts.action in ['list', 'check']:
+ getattr(dotfiles, opts.action)()
+
+ elif opts.action in ['add', 'remove']:
+ getattr(dotfiles, opts.action)(args)
+
+ elif opts.action == 'sync':
+ dotfiles.sync(opts.force)
+
+ elif opts.action == 'move':
+ if len(args) > 1:
+ print "Error: Move cannot handle multiple targets."
+ exit(-1)
+ if opts.repo != args[0]:
+ dotfiles.move(args[0])
+
+ else:
+ print "Error: Something truly terrible has happened."
+ exit(-1)
def missing_default_repo():
diff --git a/dotfiles/core.py b/dotfiles/core.py
index a1ee264..4c8add4 100644
--- a/dotfiles/core.py
+++ b/dotfiles/core.py
@@ -11,18 +11,18 @@ import os
import shutil
-__version__ = '0.3.1'
+__version__ = '0.4.0'
__author__ = "Jon Bernard"
__license__ = "GPL"
class Dotfile(object):
- def __init__(self, name, target):
+ def __init__(self, name, target, home):
if name.startswith('/'):
self.name = name
else:
- self.name = os.path.expanduser('~/.%s' % name.strip('.'))
+ self.name = os.path.expanduser(home + '/.%s' % name.strip('.'))
self.basename = os.path.basename(self.name)
self.target = target.rstrip('/')
self.status = ''
@@ -38,7 +38,10 @@ class Dotfile(object):
if not force:
print "Skipping \"%s\", use --force to override" % self.basename
return
- os.remove(self.name)
+ if os.path.isdir(self.name):
+ shutil.rmtree(self.name)
+ else:
+ os.remove(self.name)
os.symlink(self.target, self.name)
def add(self):
@@ -63,45 +66,92 @@ class Dotfile(object):
class Dotfiles(object):
+ """A Dotfiles Repository."""
+
+ __attrs__ = ['home', 'repo', 'prefix', 'ignore', 'externals']
+
+ def __init__(self, **kwargs):
+
+ # Map args from kwargs to instance-local variables
+ map(lambda k, v: (k in self.__attrs__) and setattr(self, k, v),
+ kwargs.iterkeys(), kwargs.itervalues())
+
+ self._load()
+
+
+ def _load(self):
+ """Load each dotfile in the repository."""
+
+ self.dotfiles = list()
+
+ for dotfile in list(x for x in os.listdir(self.repo) if x not in self.ignore):
+ self.dotfiles.append(Dotfile(dotfile[len(self.prefix):],
+ os.path.join(self.repo, dotfile), self.home))
+
+ for dotfile in self.externals.keys():
+ self.dotfiles.append(Dotfile(dotfile, self.externals[dotfile], self.home))
+
+
+ def _fqpn(self, dotfile):
+ """Return the fully qualified path to a dotfile."""
+
+ return os.path.join(self.repo,
+ self.prefix + os.path.basename(dotfile).strip('.'))
+
+
+ def list(self, verbose=True):
+ """List the contents of this repository."""
- def __init__(self, location, prefix, ignore, externals, force):
- self.location = location
- self.prefix = prefix
- self.force = force
- self.dotfiles = []
- contents = [x for x in os.listdir(self.location) if x not in ignore]
- for file in contents:
- self.dotfiles.append(Dotfile(file[len(prefix):],
- os.path.join(self.location, file)))
- for file in externals.keys():
- self.dotfiles.append(Dotfile(file, externals[file]))
-
- def list(self, **kwargs):
for dotfile in sorted(self.dotfiles, key=lambda dotfile: dotfile.name):
- if dotfile.status or kwargs.get('verbose', True):
+ if dotfile.status or verbose:
print dotfile
- def check(self, **kwargs):
+
+ def check(self):
+ """List only unmanaged and/or missing dotfiles."""
+
self.list(verbose=False)
- def sync(self, **kwargs):
+
+ def sync(self, force=False):
+
+ """Synchronize this repository, creating and updating the necessary
+ symbolic links."""
+
for dotfile in self.dotfiles:
- dotfile.sync(self.force)
+ dotfile.sync(force)
- def add(self, **kwargs):
- for file in kwargs.get('files', None):
- if os.path.basename(file).startswith('.'):
- Dotfile(file,
- os.path.join(self.location,
- self.prefix + os.path.basename(file).strip('.'))).add()
- else:
- print "Skipping \"%s\", not a dotfile" % file
- def remove(self, **kwargs):
- for file in kwargs.get('files', None):
+ def add(self, files):
+ """Add dotfile(s) to the repository."""
+
+ self._perform_action('add', files)
+
+
+ def remove(self, files):
+ """Remove dotfile(s) from the repository."""
+
+ self._perform_action('remove', files)
+
+
+ def _perform_action(self, action, files):
+ for file in files:
if os.path.basename(file).startswith('.'):
- Dotfile(file,
- os.path.join(self.location,
- self.prefix + os.path.basename(file).strip('.'))).remove()
+ getattr(Dotfile(file, self._fqpn(file), self.home), action)()
else:
print "Skipping \"%s\", not a dotfile" % file
+
+
+ def move(self, target):
+ """Move the repository to another location."""
+
+ if os.path.exists(target):
+ raise ValueError('Target already exists: %s' % (target))
+
+ shutil.copytree(self.repo, target)
+ shutil.rmtree(self.repo)
+
+ self.repo = target
+
+ self._load()
+ self.sync(force=True)
diff --git a/setup.py b/setup.py
index f41e668..df99c5a 100644..100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+import os
+import sys
+
try:
from setuptools import setup
except ImportError:
@@ -9,6 +12,16 @@ except ImportError:
from dotfiles.core import __version__
+if sys.argv[-1] == "publish":
+ os.system("python setup.py sdist upload")
+ sys.exit()
+
+
+if sys.argv[-1] == "test":
+ os.system("python test_dotfiles.py")
+ sys.exit()
+
+
setup(name='dotfiles',
version=__version__,
description='Easily manage your dotfiles',
diff --git a/test_dotfiles.py b/test_dotfiles.py
new file mode 100755
index 0000000..360d7db
--- /dev/null
+++ b/test_dotfiles.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from dotfiles import core
+
+
+def touch(fname, times=None):
+ with file(fname, 'a'):
+ os.utime(fname, times)
+
+
+class DotfilesTestCase(unittest.TestCase):
+
+ def setUp(self):
+ """Create a temporary home directory."""
+
+ self.home = tempfile.mkdtemp()
+
+ # create a repository for the tests to use
+ self.repo = os.path.join(self.home, 'Dotfiles')
+ os.mkdir(self.repo)
+
+ def tearDown(self):
+ """Delete the temporary home directory and its contents."""
+
+ shutil.rmtree(self.home)
+
+ def test_force_sync_directory(self):
+ """Test forced sync when the dotfile is a directory.
+
+ I installed the lastpass chrome extension which stores a socket in
+ ~/.lastpass. So I added that directory as an external to /tmp and
+ attempted a forced sync. An error occurred because sync() calls
+ os.remove() as it mistakenly assumes the dotfile is a file and not
+ a directory.
+ """
+
+ os.mkdir(os.path.join(self.home, '.lastpass'))
+ externals = {'.lastpass': '/tmp'}
+
+ dotfiles = core.Dotfiles(home=self.home, repo=self.repo, prefix='',
+ ignore=[], externals=externals)
+
+ dotfiles.sync(force=True)
+
+ self.assertEqual(
+ os.path.realpath(os.path.join(self.home, '.lastpass')), '/tmp')
+
+ def test_move_repository(self):
+ """Test the move() method for a Dotfiles repository."""
+
+ touch(os.path.join(self.repo, 'bashrc'))
+
+ dotfiles = core.Dotfiles(
+ home=self.home, repo=self.repo, prefix='',
+ ignore=[], force=True, externals={})
+
+ dotfiles.sync()
+
+ # make sure sync() did the right thing
+ self.assertEqual(
+ os.path.realpath(os.path.join(self.home, '.bashrc')),
+ os.path.join(self.repo, 'bashrc'))
+
+ target = os.path.join(self.home, 'MyDotfiles')
+
+ dotfiles.move(target)
+
+ self.assertTrue(os.path.exists(os.path.join(target, 'bashrc')))
+ self.assertEqual(
+ os.path.realpath(os.path.join(self.home, '.bashrc')),
+ os.path.join(target, 'bashrc'))
+
+
+def suite():
+ suite = unittest.TestLoader().loadTestsFromTestCase(DotfilesTestCase)
+ return suite
+
+if __name__ == '__main__':
+ unittest.TextTestRunner().run(suite())