summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS.rst5
-rw-r--r--LICENSE.rst (renamed from LICENSE)7
-rw-r--r--README.rst33
-rw-r--r--contrib/dotfilesrc13
-rw-r--r--dotfiles/cli.py268
-rw-r--r--dotfiles/core.py24
-rwxr-xr-xsetup.py1
-rwxr-xr-xtest_dotfiles.py63
8 files changed, 228 insertions, 186 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst
index a183cc8..dc83965 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -1,7 +1,7 @@
Dotfiles is written and maintained by Jon Bernard and various contributors:
-Development Lead
-````````````````
+Development
+```````````
- Jon Bernard <jbernard@tuxion.com>
@@ -11,3 +11,4 @@ Patches and Suggestions
- Anaƫl Beutot
- Remco Wendt <remco@maykinmedia.nl>
+- Sebastian Rahlf
diff --git a/LICENSE b/LICENSE.rst
index 88f69aa..c45726b 100644
--- a/LICENSE
+++ b/LICENSE.rst
@@ -1,4 +1,9 @@
-Copyright (C) 2011 Jon Bernard <jbernard@tuxion.com>
+License
+-------
+
+GPL License. ::
+
+Copyright (C) 2011 Jon Bernard and contributers
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/README.rst b/README.rst
index 0509f1f..22813c5 100644
--- a/README.rst
+++ b/README.rst
@@ -4,7 +4,7 @@ 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. Using whatever VCS you prefer, or even rsync, you can
+Hosting is up to you. Using whatever VCS you prefer, or even rsync, you can
easily distribute your dotfiles repository across multiple hosts.
Interface
@@ -73,9 +73,9 @@ You get the idea. Type ``dotfiles --help`` to see the available options.
Configuration
-------------
-You can choose to create a configuration file to store personal
-customizations. By default, ``dotfiles`` will look in ``~/.dotfilesrc``. An
-example configuration file might look like: ::
+You can choose to create a configuration file to store personal customizations.
+By default, ``dotfiles`` will look for ``~/.dotfilesrc``. You can change this
+with the ``-C`` flag. An example configuration file might look like: ::
[dotfiles]
repository = ~/Dotfiles
@@ -87,6 +87,11 @@ example configuration file might look like: ::
'.bzr.log': '/dev/null',
'.uml': '/tmp'}
+You can also store your configuration file inside your repository. Put your
+settings in ``.dotfilesrc`` at the root of your repository and ``dotfiles`` will
+find it. Note that ``ignore`` and ``externals`` are appended to any values
+previously discovered.
+
Prefixes
--------
@@ -131,26 +136,6 @@ I have the following in my ``~/.dotfilesrc``: ::
Any file you list in ``ignore`` will be skipped. The ``ignore`` option supports
glob file patterns.
-License
--------
-
-GPL License. ::
-
- Copyright (C) 2011 Jon Bernard
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
Contribute
----------
diff --git a/contrib/dotfilesrc b/contrib/dotfilesrc
deleted file mode 100644
index f26515e..0000000
--- a/contrib/dotfilesrc
+++ /dev/null
@@ -1,13 +0,0 @@
-[dotfiles]
-repository = ~/Dotfiles
-prefix =
-ignore = [
- '.metadata',
- '.git',
- '.gitignore']
-externals = {
- '.adobe': '/tmp',
- '.bzr.log': '/dev/null',
- '.lastpass': '/tmp',
- '.macromedia': '/tmp',
- '.uml': '/tmp'}
diff --git a/dotfiles/cli.py b/dotfiles/cli.py
index a056f70..3441e75 100644
--- a/dotfiles/cli.py
+++ b/dotfiles/cli.py
@@ -13,166 +13,226 @@ from . import core
import ConfigParser
from optparse import OptionParser, OptionGroup
-DEFAULT_REPO = os.path.expanduser('~/Dotfiles')
+defaults = {
+ 'prefix': '',
+ 'homedir': '~/',
+ 'repository': '~/Dotfiles',
+ 'config_file': '~/.dotfilesrc'}
+settings = {
+ 'prefix': None,
+ 'homedir': None,
+ 'repository': None,
+ 'config_file': None,
+ 'ignore': set(['.dotfilesrc']),
+ 'externals': dict()}
-def parse_args():
- parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]")
+def missing_default_repo():
+ """Print a helpful message when the default repository is missing."""
+
+ print """
+If this is your first time running dotfiles, you must first create
+a repository. By default, dotfiles will look for '{0}'.
+Something like:
+
+ $ mkdir {0}
+
+is all you need to do. If you don't like the default, you can put your
+repository wherever you like. You have two choices once you've created your
+repository. You can specify the path to the repository on the command line
+using the '-R' flag. Alternatively, you can create a configuration file at
+'~/.dotfilesrc' and place the path to your repository in there. The contents
+would look like:
- parser.set_defaults(config=os.path.expanduser("~/.dotfilesrc"))
- parser.set_defaults(ignore=[])
- parser.set_defaults(externals={})
+ [dotfiles]
+ repository = {0}
+
+Type 'dotfiles -h' to see detailed usage information.""".format(
+ defaults['repository'])
- parser.add_option("-v", "--version", action="store_true",
- dest="show_version", default=False,
+
+def add_global_flags(parser):
+ parser.add_option("-v", "--version",
+ action="store_true", dest="show_version", default=False,
help="show version number and exit")
- parser.add_option("-f", "--force", action="store_true", dest="force",
- default=False, help="ignore unmanaged dotfiles (use with --sync)")
+ parser.add_option("-f", "--force",
+ action="store_true", dest="force", default=False,
+ help="ignore unmanaged dotfiles (use with --sync)")
+
+ parser.add_option("-R", "--repo",
+ type="string", dest="repository",
+ help="set repository location (default: %s)" % (
+ defaults['repository']))
- # OptionParser expands ~ constructions
- parser.add_option("-R", "--repo", type="string", dest="repo",
- help="set repository location (default is %s)" % DEFAULT_REPO)
+ parser.add_option("-p", "--prefix",
+ type="string", dest="prefix",
+ help="set prefix character (default: %s)" % (
+ "None" if not defaults['prefix'] else defaults['prefix']))
- parser.add_option("-p", "--prefix", type="string", dest="prefix",
- help="set prefix character (default is None)")
+ parser.add_option("-C", "--config",
+ type="string", dest="config_file",
+ help="set configuration file location (default: %s)" % (
+ defaults['config_file']))
- 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="homedir",
+ help="set home directory location (default: %s)" % (
+ defaults['homedir']))
- parser.add_option("-H", "--home", type="string", dest="home",
- help="set home directory location (default is ~/)")
+def add_action_group(parser):
action_group = OptionGroup(parser, "Actions")
- action_group.add_option("-a", "--add", action="store_const", dest="action",
- const="add", help="add dotfile(s) to the repository")
+ action_group.add_option("-a", "--add",
+ action="store_const", dest="action", const="add",
+ help="add dotfile(s) to the repository")
- action_group.add_option("-c", "--check", action="store_const",
- dest="action", const="check", help="check dotfiles repository")
+ action_group.add_option("-c", "--check",
+ action="store_const", dest="action", const="check",
+ help="check for broken and unmanaged dotfiles")
- action_group.add_option("-l", "--list", action="store_const",
- dest="action", const="list",
+ action_group.add_option("-l", "--list",
+ action="store_const", dest="action", const="list",
help="list currently managed dotfiles")
- action_group.add_option("-r", "--remove", action="store_const",
- dest="action", const="remove",
+ action_group.add_option("-r", "--remove",
+ action="store_const", dest="action", const="remove",
help="remove dotfile(s) from the repository")
- action_group.add_option("-s", "--sync", action="store_const",
- dest="action", const="sync", help="update dotfile symlinks")
+ 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")
+ 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()
-
- # Skip checking if the repository exists here. The user may have specified
- # a command line argument or a configuration file, which will be examined
- # next.
- return (opts, args)
+def parse_args():
+ parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]")
-def main():
+ add_global_flags(parser)
+ add_action_group(parser)
- (opts, args) = parse_args()
+ (opts, args) = parser.parse_args()
if opts.show_version:
print 'dotfiles v%s' % core.__version__
exit(0)
- config_defaults = {
- 'repository': opts.repo,
- 'prefix': opts.prefix,
- 'ignore': opts.ignore,
- 'externals': opts.externals}
+ if not opts.action:
+ print "Error: An action is required. Type 'dotfiles -h' to see " \
+ "detailed usage information."
+ exit(-1)
- parser = ConfigParser.SafeConfigParser(config_defaults)
- parser.read(opts.config)
+ return (opts, args)
- if 'dotfiles' in parser.sections():
- if not opts.repo and parser.get('dotfiles', 'repository'):
- opts.repo = 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)
+def parse_config(config_file):
- if not opts.prefix and parser.get('dotfiles', 'prefix'):
- opts.prefix = parser.get('dotfiles', 'prefix')
+ parser = ConfigParser.SafeConfigParser()
+ parser.read(config_file)
- if not opts.ignore and parser.get('dotfiles', 'ignore'):
- opts.ignore = eval(parser.get('dotfiles', 'ignore'))
+ opts = {'repository': None,
+ 'prefix': None,
+ 'ignore': set(),
+ 'externals': dict()}
- if not opts.externals and parser.get('dotfiles', 'externals'):
- opts.externals = eval(parser.get('dotfiles', 'externals'))
+ for entry in ('repository', 'prefix'):
+ try:
+ opts[entry] = parser.get('dotfiles', entry)
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ break
- if not opts.repo:
- opts.repo = DEFAULT_REPO
+ for entry in ('ignore', 'externals'):
+ try:
+ opts[entry] = eval(parser.get('dotfiles', entry))
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ break
- opts.repo = os.path.realpath(os.path.expanduser(opts.repo))
+ return opts
- if not opts.prefix:
- opts.prefix = ''
- if not os.path.exists(opts.repo):
- print 'Error: Could not find dotfiles repository \"%s\"' % (opts.repo)
- if opts.repo == DEFAULT_REPO:
- missing_default_repo()
+def dispatch(dotfiles, action, force, args):
+ if action in ['list', 'check']:
+ getattr(dotfiles, action)()
+ elif action in ['add', 'remove']:
+ getattr(dotfiles, action)(args)
+ elif action == 'sync':
+ dotfiles.sync(force)
+ elif action == 'move':
+ if len(args) > 1:
+ print "Error: Move cannot handle multiple targets."
+ exit(-1)
+ dotfiles.move(args[0])
+ else:
+ print "Error: Something truly terrible has happened."
exit(-1)
- if not opts.action:
- print "Error: An action is required. Type 'dotfiles -h' to see detailed usage information."
+
+def compare_path(path1, path2):
+ return (os.path.realpath(os.path.expanduser(path1)) ==
+ os.path.realpath(os.path.expanduser(path2)))
+
+
+def realpath(path):
+ return os.path.realpath(os.path.expanduser(path))
+
+
+def check_repository_exists():
+ if not os.path.exists(settings['repository']):
+ print 'Error: Could not find dotfiles repository \"%s\"' % (
+ settings['repository'])
+ if compare_path(settings['repository'], defaults['repository']):
+ missing_default_repo()
exit(-1)
- dotfiles = core.Dotfiles(home='~/', repo=opts.repo, prefix=opts.prefix,
- ignore=opts.ignore, externals=opts.externals)
+def update_settings(opts, key):
+ global settings
- if opts.action in ['list', 'check']:
- getattr(dotfiles, opts.action)()
+ settings[key].update(opts[key])
- elif opts.action in ['add', 'remove']:
- getattr(dotfiles, opts.action)(args)
- elif opts.action == 'sync':
- dotfiles.sync(opts.force)
+def main():
- 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])
+ global settings
- else:
- print "Error: Something truly terrible has happened."
- exit(-1)
+ (cli_opts, args) = parse_args()
+ settings['homedir'] = realpath(cli_opts.homedir or defaults['homedir'])
+ settings['config_file'] = realpath(cli_opts.config_file or
+ defaults['config_file'])
-def missing_default_repo():
- """Print a helpful message when the default repository is missing."""
+ config_opts = parse_config(settings['config_file'])
- print """
-If this is your first time running dotfiles, you must first create
-a repository. By default, dotfiles will look for '{0}'.
-Something like:
+ settings['repository'] = realpath(cli_opts.repository or
+ config_opts['repository'] or defaults['repository'])
- $ mkdir {0}
+ check_repository_exists()
-is all you need to do. If you don't like the default, you can put your
-repository wherever you like. You have two choices once you've created your
-repository. You can specify the path to the repository on the command line
-using the '-R' flag. Alternatively, you can create a configuration file at
-'~/.dotfilesrc' and place the path to your repository in there. The contents
-would look like:
+ update_settings(config_opts, 'ignore')
+ update_settings(config_opts, 'externals')
- [dotfiles]
- repository = {0}
+ repo_config_file = os.path.join(settings['repository'], '.dotfilesrc')
+ repo_config_opts = parse_config(repo_config_file)
+
+ settings['prefix'] = (cli_opts.prefix or
+ repo_config_opts['prefix'] or
+ config_opts['prefix'] or
+ defaults['prefix'])
+
+ update_settings(repo_config_opts, 'ignore')
+ update_settings(repo_config_opts, 'externals')
+
+ dotfiles = core.Dotfiles(**settings)
-Type 'dotfiles -h' to see detailed usage information.""".format(DEFAULT_REPO)
+ dispatch(dotfiles, cli_opts.action, cli_opts.force, args)
diff --git a/dotfiles/core.py b/dotfiles/core.py
index a1aec45..f6c68c1 100644
--- a/dotfiles/core.py
+++ b/dotfiles/core.py
@@ -23,7 +23,7 @@ class Dotfile(object):
if name.startswith('/'):
self.name = name
else:
- self.name = os.path.expanduser(home + '/.%s' % name.strip('.'))
+ self.name = home + '/.%s' % name.strip('.')
self.basename = os.path.basename(self.name)
self.target = target.rstrip('/')
self.status = ''
@@ -69,7 +69,7 @@ class Dotfile(object):
class Dotfiles(object):
"""A Dotfiles Repository."""
- __attrs__ = ['home', 'repo', 'prefix', 'ignore', 'externals']
+ __attrs__ = ['homedir', 'repository', 'prefix', 'ignore', 'externals']
def __init__(self, **kwargs):
@@ -84,23 +84,25 @@ class Dotfiles(object):
self.dotfiles = list()
- all_repofiles = os.listdir(self.repo)
+ all_repofiles = os.listdir(self.repository)
repofiles_to_symlink = set(all_repofiles)
for pat in self.ignore:
- repofiles_to_symlink.difference_update(fnmatch.filter(all_repofiles, pat))
+ repofiles_to_symlink.difference_update(
+ fnmatch.filter(all_repofiles, pat))
for dotfile in repofiles_to_symlink:
self.dotfiles.append(Dotfile(dotfile[len(self.prefix):],
- os.path.join(self.repo, dotfile), self.home))
+ os.path.join(self.repository, dotfile), self.homedir))
for dotfile in self.externals.keys():
- self.dotfiles.append(Dotfile(dotfile, self.externals[dotfile], self.home))
+ self.dotfiles.append(Dotfile(dotfile, self.externals[dotfile],
+ self.homedir))
def _fqpn(self, dotfile):
"""Return the fully qualified path to a dotfile."""
- return os.path.join(self.repo,
+ return os.path.join(self.repository,
self.prefix + os.path.basename(dotfile).strip('.'))
def list(self, verbose=True):
@@ -136,7 +138,7 @@ class Dotfiles(object):
def _perform_action(self, action, files):
for file in files:
if os.path.basename(file).startswith('.'):
- getattr(Dotfile(file, self._fqpn(file), self.home), action)()
+ getattr(Dotfile(file, self._fqpn(file), self.homedir), action)()
else:
print "Skipping \"%s\", not a dotfile" % file
@@ -146,10 +148,10 @@ class Dotfiles(object):
if os.path.exists(target):
raise ValueError('Target already exists: %s' % (target))
- shutil.copytree(self.repo, target)
- shutil.rmtree(self.repo)
+ shutil.copytree(self.repository, target)
+ shutil.rmtree(self.repository)
- self.repo = target
+ self.repository = target
self._load()
self.sync(force=True)
diff --git a/setup.py b/setup.py
index e11410b..098e351 100755
--- a/setup.py
+++ b/setup.py
@@ -26,6 +26,7 @@ setup(name='dotfiles',
version=__version__,
description='Easily manage your dotfiles',
long_description=open('README.rst').read() + '\n\n' +
+ open('LICENSE.rst').read() + '\n\n' +
open('HISTORY.rst').read(),
author='Jon Bernard',
author_email='jbernard@tuxion.com',
diff --git a/test_dotfiles.py b/test_dotfiles.py
index 85cdce6..f9b6b46 100755
--- a/test_dotfiles.py
+++ b/test_dotfiles.py
@@ -21,16 +21,16 @@ class DotfilesTestCase(unittest.TestCase):
def setUp(self):
"""Create a temporary home directory."""
- self.home = tempfile.mkdtemp()
+ self.homedir = tempfile.mkdtemp()
# Create a repository for the tests to use.
- self.repo = os.path.join(self.home, 'Dotfiles')
- os.mkdir(self.repo)
+ self.repository = os.path.join(self.homedir, 'Dotfiles')
+ os.mkdir(self.repository)
def tearDown(self):
"""Delete the temporary home directory and its contents."""
- shutil.rmtree(self.home)
+ shutil.rmtree(self.homedir)
def assertPathEqual(self, path1, path2):
self.assertEqual(
@@ -47,41 +47,42 @@ class DotfilesTestCase(unittest.TestCase):
a directory.
"""
- os.mkdir(os.path.join(self.home, '.lastpass'))
+ os.mkdir(os.path.join(self.homedir, '.lastpass'))
externals = {'.lastpass': '/tmp'}
- dotfiles = core.Dotfiles(home=self.home, repo=self.repo, prefix='',
- ignore=[], externals=externals)
+ dotfiles = core.Dotfiles(
+ homedir=self.homedir, repository=self.repository,
+ prefix='', ignore=[], externals=externals)
dotfiles.sync(force=True)
self.assertPathEqual(
- os.path.join(self.home, '.lastpass'),
+ os.path.join(self.homedir, '.lastpass'),
'/tmp')
def test_move_repository(self):
"""Test the move() method for a Dotfiles repository."""
- touch(os.path.join(self.repo, 'bashrc'))
+ touch(os.path.join(self.repository, 'bashrc'))
dotfiles = core.Dotfiles(
- home=self.home, repo=self.repo, prefix='',
- ignore=[], force=True, externals={})
+ homedir=self.homedir, repository=self.repository,
+ prefix='', ignore=[], force=True, externals={})
dotfiles.sync()
# Make sure sync() did the right thing.
self.assertPathEqual(
- os.path.join(self.home, '.bashrc'),
- os.path.join(self.repo, 'bashrc'))
+ os.path.join(self.homedir, '.bashrc'),
+ os.path.join(self.repository, 'bashrc'))
- target = os.path.join(self.home, 'MyDotfiles')
+ target = os.path.join(self.homedir, 'MyDotfiles')
dotfiles.move(target)
self.assertTrue(os.path.exists(os.path.join(target, 'bashrc')))
self.assertPathEqual(
- os.path.join(self.home, '.bashrc'),
+ os.path.join(self.homedir, '.bashrc'),
os.path.join(target, 'bashrc'))
def test_sync_unmanaged_directory_symlink(self):
@@ -95,29 +96,29 @@ class DotfilesTestCase(unittest.TestCase):
"""
# Create a dotfile symlink to some directory
- os.mkdir(os.path.join(self.home, 'vim'))
- os.symlink(os.path.join(self.home, 'vim'),
- os.path.join(self.home, '.vim'))
+ os.mkdir(os.path.join(self.homedir, 'vim'))
+ os.symlink(os.path.join(self.homedir, 'vim'),
+ os.path.join(self.homedir, '.vim'))
# Create a vim directory in the repository. This will cause the above
# symlink to be overwritten on sync.
- os.mkdir(os.path.join(self.repo, 'vim'))
+ os.mkdir(os.path.join(self.repository, 'vim'))
# Make sure the symlink points to the correct location.
self.assertPathEqual(
- os.path.join(self.home, '.vim'),
- os.path.join(self.home, 'vim'))
+ os.path.join(self.homedir, '.vim'),
+ os.path.join(self.homedir, 'vim'))
dotfiles = core.Dotfiles(
- home=self.home, repo=self.repo, prefix='',
- ignore=[], externals={})
+ homedir=self.homedir, repository=self.repository,
+ prefix='', ignore=[], externals={})
dotfiles.sync(force=True)
# The symlink should now point to the directory in the repository.
self.assertPathEqual(
- os.path.join(self.home, '.vim'),
- os.path.join(self.repo, 'vim'))
+ os.path.join(self.homedir, '.vim'),
+ os.path.join(self.repository, 'vim'))
def test_glob_ignore_pattern(self):
""" Test that the use of glob pattern matching works in the ignores list.
@@ -156,11 +157,11 @@ class DotfilesTestCase(unittest.TestCase):
all_dotfiles = [f for f in all_repo_files if f[1] is not None]
for original, symlink in all_repo_files:
- touch(os.path.join(self.repo, original))
+ touch(os.path.join(self.repository, original))
dotfiles = core.Dotfiles(
- home=self.home, repo=self.repo, prefix='',
- ignore=ignore, externals={})
+ homedir=self.homedir, repository=self.repository,
+ prefix='', ignore=ignore, externals={})
dotfiles.sync()
@@ -168,13 +169,13 @@ class DotfilesTestCase(unittest.TestCase):
# point to the correct file and are the only files that
# exist in the home dir.
self.assertEqual(
- sorted(os.listdir(self.home)),
+ sorted(os.listdir(self.homedir)),
sorted([f[1] for f in all_dotfiles] + ['Dotfiles']))
for original, symlink in all_dotfiles:
self.assertPathEqual(
- os.path.join(self.repo, original),
- os.path.join(self.home, symlink))
+ os.path.join(self.repository, original),
+ os.path.join(self.homedir, symlink))
def suite():
suite = unittest.TestLoader().loadTestsFromTestCase(DotfilesTestCase)