diff options
author | Jon Bernard <jbernard@tuxion.com> | 2011-10-30 23:27:17 -0400 |
---|---|---|
committer | Jon Bernard <jbernard@tuxion.com> | 2011-11-01 09:21:39 -0400 |
commit | 4584eb5790d42092ba1d3965ae987fef7b4d8c43 (patch) | |
tree | dda86e37f15d10a8a1373448c4996774a500415b | |
parent | 6aec9c0e3c3e8a5e7718f5c34bb620462cb724f3 (diff) | |
download | dotfiles-4584eb5790d42092ba1d3965ae987fef7b4d8c43.tar.gz dotfiles-4584eb5790d42092ba1d3965ae987fef7b4d8c43.tar.bz2 dotfiles-4584eb5790d42092ba1d3965ae987fef7b4d8c43.zip |
Add support for in-repo configuration files
In addition to parsing .dotfilesrc in the user's home directory, also look for
that file inside the repository. This allows you to keep your repository and
it's settings together.
Closes: #1
-rw-r--r-- | AUTHORS.rst | 5 | ||||
-rw-r--r-- | README.rst | 13 | ||||
-rw-r--r-- | dotfiles/cli.py | 268 | ||||
-rw-r--r-- | dotfiles/core.py | 24 | ||||
-rwxr-xr-x | test_dotfiles.py | 63 |
5 files changed, 221 insertions, 152 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 @@ -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 -------- 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/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) |