diff options
-rw-r--r-- | AUTHORS.rst | 5 | ||||
-rw-r--r-- | LICENSE.rst (renamed from LICENSE) | 7 | ||||
-rw-r--r-- | README.rst | 33 | ||||
-rw-r--r-- | contrib/dotfilesrc | 13 | ||||
-rw-r--r-- | dotfiles/cli.py | 268 | ||||
-rw-r--r-- | dotfiles/core.py | 24 | ||||
-rwxr-xr-x | setup.py | 1 | ||||
-rwxr-xr-x | test_dotfiles.py | 63 |
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 @@ -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 @@ -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) @@ -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) |