aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Jon Bernard <jbernard@tuxion.com> 2016-01-03 14:33:01 -0500
committerGravatar Jon Bernard <jbernard@tuxion.com> 2016-01-03 22:39:43 -0500
commit48e781a75c8b5d20c1e9c54c92dd31e641dfd121 (patch)
tree75616c75f936c9677777662306893feca97b9270
parent863726c2d0b747686ae906e7a6016cee23602090 (diff)
downloaddotfiles-48e781a75c8b5d20c1e9c54c92dd31e641dfd121.tar.gz
dotfiles-48e781a75c8b5d20c1e9c54c92dd31e641dfd121.tar.bz2
dotfiles-48e781a75c8b5d20c1e9c54c92dd31e641dfd121.zip
Consolidate new implementation into a single file
-rw-r--r--dotfiles.py226
-rw-r--r--dotfiles/__init__.py14
-rw-r--r--dotfiles/cli.py53
-rw-r--r--dotfiles/cli_orig.py254
-rw-r--r--dotfiles/compat.py214
-rw-r--r--dotfiles/core.py269
-rw-r--r--dotfiles/dotfile.py77
-rw-r--r--dotfiles/repository.py61
-rw-r--r--dotfiles/utils.py22
-rw-r--r--setup.py10
10 files changed, 232 insertions, 968 deletions
diff --git a/dotfiles.py b/dotfiles.py
new file mode 100644
index 0000000..1e087a1
--- /dev/null
+++ b/dotfiles.py
@@ -0,0 +1,226 @@
+import py
+import os
+import click
+import errno
+from operator import attrgetter
+
+
+__version__ = '0.7-dev'
+
+default_home = os.path.expanduser('~/')
+default_repo = os.path.expanduser('~/Dotfiles')
+
+
+def unique_suffix(path_a, path_b):
+ discard = len(str(path_a.common(path_b))) + 1
+ return (str(path_a)[discard:], str(path_b)[discard:])
+
+
+class Repository(object):
+ """
+ This class implements the 'repository' abstraction.
+
+ A repository is a directory that contains dotfiles. It has two primary
+ attributes:
+
+ repodir -- the location of the repository directory
+ homedir -- the location of the home directory (primarily for testing)
+ """
+
+ def __init__(self, repodir, homedir):
+ self.repodir = repodir
+ self.homedir = homedir
+
+ def __str__(self):
+ """Convert repository contents to human readable form."""
+ return ''.join('%s\n' % item for item in self.contents()).rstrip()
+
+ def __repr__(self):
+ return '<Repository %r>' % self.repodir
+
+ def expected_name(self, target):
+ """Given a repository target, return the expected symlink name."""
+ return py.path.local("%s/.%s" % (self.homedir, target.basename))
+
+ def contents(self):
+ """Given a repository path, discover all existing dotfiles."""
+ contents = []
+ self.repodir.ensure(dir=1)
+ for target in self.repodir.listdir():
+ target = py.path.local(target)
+ contents.append(Dotfile(self.expected_name(target), target))
+ return sorted(contents, key=attrgetter('name'))
+
+
+class Dotfile(object):
+ """
+ This class implements the 'dotfile' abstraction.
+
+ A dotfile has two primary attributes:
+
+ name -- name of symlink in the home directory (~/.vimrc)
+ target -- where the symlink should point to (~/Dotfiles/vimrc)
+
+ The above attributes are both py.path.local objects.
+
+ The goal is for there to be no special logic or stored global state. Only
+ the implementation of three operations made available to the caller:
+
+ add -- move a dotfile into the repository and replace it with a symlink
+ remove -- the opposite of add
+ sync -- ensure that each repository file has a corresponding symlink
+ unsync -- remove the symlink leaving only the repository file
+
+ This is where most filesystem operations (link, delete, etc) should be
+ called, and not in the layers above.
+ """
+
+ def __init__(self, name, target):
+ self.name = name
+ self.target = target
+
+ def __str__(self):
+ short_name, _ = unique_suffix(self.name, self.target)
+ return '%s' % short_name
+
+ def __repr__(self):
+ return '<Dotfile %r>' % self.name
+
+ @property
+ def state(self):
+
+ # lets be optimistic
+ state = 'ok'
+
+ if self.target.check(exists=0):
+ # only for testing, cli should never reach this state
+ state = 'error'
+ elif self.name.check(exists=0):
+ # no $HOME symlink
+ state = 'missing'
+ elif self.name.check(link=0) or not self.name.samefile(self.target):
+ # if name exists but isn't a link to the target
+ state = 'conflict'
+
+ return state
+
+ def add(self):
+ if self.target.check(exists=1):
+ raise OSError(errno.EEXIST, self.target)
+ self.name.move(self.target)
+ self.sync()
+
+ def remove(self):
+ if self.target.check(exists=0):
+ raise OSError(errno.ENOENT, self.target)
+ self.name.remove()
+ self.target.move(self.name)
+
+ def sync(self):
+ self.name.mksymlinkto(self.target)
+
+ def unsync(self):
+ self.name.remove()
+
+
+pass_repo = click.make_pass_decorator(Repository)
+
+
+@click.group()
+@click.option('--home-directory', type=click.Path(), default=str(default_home),
+ show_default=True)
+@click.option('--repository', type=click.Path(), default=str(default_repo),
+ show_default=True)
+@click.pass_context
+def cli(ctx, home_directory, repository):
+ """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy,
+ allowing you to keep all your dotfiles in a single directory.
+ """
+ ctx.obj = Repository(py.path.local(repository),
+ py.path.local(home_directory))
+
+
+@cli.command()
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def add(repo, files):
+ """Move dotifles into a repository."""
+ for filename in files:
+ Dotfile(filename, repo.target(filename)).add()
+
+
+@cli.command()
+@click.option('-v', '--verbose', is_flag=True, help='Show dotfile state.')
+@pass_repo
+def list(repo, verbose):
+ """Show the contents of a repository."""
+ dotfiles = repo.contents()
+ for dotfile in dotfiles:
+ if (verbose):
+ click.echo('%-18s (%s)' % (dotfile, dotfile.state))
+ else:
+ click.echo('%s' % dotfile)
+
+
+@cli.command()
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def remove(repo, files):
+ """Remove dotfiles from a repository."""
+ for filename in files:
+ Dotfile(filename, repo.target(filename)).remove()
+
+
+@cli.command()
+@click.option('-c', '--color', is_flag=True, help='Enable color.')
+@click.option('-s', '--short', is_flag=True, help='Terse output.')
+@pass_repo
+def status(repo, color, short):
+ """Show all dotifles in a non-ok state."""
+
+ states = {
+ 'error': {'char': 'E', 'color': 'red'},
+ 'conflict': {'char': '!', 'color': 'magenta'},
+ 'missing': {'char': '?', 'color': 'yellow'},
+ }
+
+ if not short:
+ raise NotImplementedError('long output, use --short for now')
+
+ dotfiles = repo.contents()
+ for dotfile in dotfiles:
+ try:
+ state_str = states[dotfile.state]['char']
+ color_str = states[dotfile.state]['color']
+ if color:
+ click.secho('%s %s' % (state_str, dotfile), fg=color_str)
+ else:
+ click.echo('%s %s' % (state_str, dotfile))
+ except KeyError:
+ continue
+
+
+@cli.command()
+@click.argument('files', nargs=-1, type=click.Path())
+@pass_repo
+def sync(repo, files):
+ """TODO"""
+ for filename in files:
+ repo.sync(filename)
+
+ # TODO: path need not exist...
+
+
+@cli.command()
+@click.argument('files', nargs=-1, type=click.Path(exists=True))
+@pass_repo
+def unsync(repo, files):
+ """TODO"""
+ for filename in files:
+ repo.unsync(filename)
+
+
+@cli.command()
+def version():
+ """Show the version number."""
+ click.echo("dotfiles version %s" % __version__)
diff --git a/dotfiles/__init__.py b/dotfiles/__init__.py
deleted file mode 100644
index 226c06e..0000000
--- a/dotfiles/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""
- dotfiles
- ~~~~~~~~
-
- 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
- up to you. You can use a VCS like git, Dropbox, or even rsync to distribute
- your dotfiles repository across multiple hosts.
-
- :copyright: (c) 2011-2014 by Jon Bernard.
- :license: ISC, see LICENSE.rst for more details.
-"""
-
-__version__ = '0.6.4'
diff --git a/dotfiles/cli.py b/dotfiles/cli.py
deleted file mode 100644
index 5ca6771..0000000
--- a/dotfiles/cli.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import sys
-import click
-
-from . import __version__
-from .repository import Repository
-
-
-pass_repo = click.make_pass_decorator(Repository)
-
-
-@click.group()
-@click.option('-r', '--repository', type=click.Path(), default='~/Dotfiles',
- help='Sets the repository folder location.')
-@click.pass_context
-def main(ctx, repository):
- """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy,
- allowing you to keep all your dotfiles in a single directory.
-
- The default repository is ~/Dotfiles unless specified otherwise and will be
- created on demand. If you prefer a different location, you can put your
- repository wherever you like using the --repository flag or using the
- ~/.dotfilesrc configuration file with a contents of:
-
- \b
- [dotfiles]
- repository = ~/Dotfiles
- """
- ctx.obj = Repository(repository)
-
-
-@main.command()
-@click.option('--color', is_flag=True, help='Enables colored output.')
-@pass_repo
-def check(repo, color):
- """Shows any broken or unsyned dotfiles."""
- list = repo.check()
- if list:
- click.echo_via_pager(list)
- sys.exit(1)
-
-
-@main.command()
-@click.option('--color', is_flag=True, help='Enables colored output.')
-@pass_repo
-def status(repo, color):
- """Shows the status of each dotfile."""
- click.echo_via_pager(repo.status())
-
-
-@main.command()
-def version():
- """Shows the current version number."""
- click.echo("dotfiles v%s" % __version__)
diff --git a/dotfiles/cli_orig.py b/dotfiles/cli_orig.py
deleted file mode 100644
index 19bf234..0000000
--- a/dotfiles/cli_orig.py
+++ /dev/null
@@ -1,254 +0,0 @@
-import os
-try:
- import ConfigParser as configparser
-except ImportError:
- import configparser
-from optparse import OptionParser, OptionGroup
-
-from .utils import compare_path, realpath_expanduser
-from .core import Dotfiles as Repository
-from . import __version__
-
-
-CONFIG_FILE = '.dotfilesrc'
-
-
-# Users can define configuration at several different levels to overlay
-# specific configuration for a particular repository. These settings are
-# accumulated and passed to the Repository constructor once parsing has
-# completed.
-repo_settings = {
- 'path': Repository.defaults['path'],
- 'prefix': Repository.defaults['prefix'],
- 'ignore': Repository.defaults['ignore'],
- 'homedir': Repository.defaults['homedir'],
- 'packages': Repository.defaults['packages'],
- 'externals': Repository.defaults['externals'],
-}
-
-
-def missing_default_repo():
- """Print a helpful message when the default repository is missing.
-
- For a first-time user, this is the first message they're likely to see, so
- it should be as helpful as possible.
- """
-
- 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
-'~/{1}' and place the path to your repository in there. The contents would
-look like:
-
- [dotfiles]
- repository = {0}
-
-Type 'dotfiles -h' to see detailed usage information.""".format
- (repo_settings['path'], CONFIG_FILE))
-
-
-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="overwrite colliding dotfiles (use with --sync)")
-
- parser.add_option("-R", "--repo",
- type="string", dest="repository",
- help="set repository location (default: %s)" % (
- repo_settings['path']))
-
- parser.add_option("-p", "--prefix",
- type="string", dest="prefix",
- help="set prefix character (default: %s)" % (
- None if not repo_settings['prefix'] else
- repo_settings['prefix']))
-
- parser.add_option("-C", "--config",
- type="string", dest="config_file",
- help="set configuration file (default: ~/%s)" % (
- CONFIG_FILE))
-
- parser.add_option("-H", "--home",
- type="string", dest="homedir",
- help="set home directory location (default: %s)" % (
- repo_settings['homedir']))
-
- parser.add_option("-d", "--dry-run",
- action="store_true", default=False,
- help="don't modify anything, just print commands")
-
- parser.add_option("-n", "--no-dot-prefix",
- action="store_true", default=False,
- help="don't prefix symlinks in target directory " +
- "with a '.'")
-
-
-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("-c", "--check",
- action="store_const", dest="action", const="check",
- help="check for broken and unsynced dotfiles")
-
- 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",
- 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("-m", "--move",
- action="store_const", dest="action", const="move",
- help="move (rename) dotfiles repository")
-
- parser.add_option_group(action_group)
-
-
-def parse_args():
-
- parser = OptionParser(usage="%prog ACTION [OPTION...] [FILE...]")
-
- add_global_flags(parser)
- add_action_group(parser)
-
- (opts, args) = parser.parse_args()
-
- if opts.show_version:
- print('dotfiles v%s' % __version__)
- exit(0)
-
- if not opts.action:
- print("Error: An action is required. Type 'dotfiles -h' to see "
- "detailed usage information.")
- exit(-1)
-
- return (opts, args)
-
-
-def parse_config(config_file):
- parser = configparser.SafeConfigParser()
- parser.read(config_file)
-
- opts = dict()
-
- for entry in ('repository', 'prefix'):
- try:
- opts[entry] = parser.get('dotfiles', entry)
- except configparser.NoOptionError:
- pass
- except configparser.NoSectionError:
- break
-
- for entry in ('ignore', 'externals', 'packages'):
- try:
- opts[entry] = eval(parser.get('dotfiles', entry))
- except configparser.NoOptionError:
- pass
- except configparser.NoSectionError:
- break
-
- return opts
-
-
-def dispatch(repo, opts, args):
-
- # TODO: handle/pass dry_run
-
- if opts.action in ['list', 'check']:
- getattr(repo, opts.action)()
-
- elif opts.action in ['add', 'remove']:
- getattr(repo, opts.action)(args)
-
- elif opts.action == 'sync':
- getattr(repo, opts.action)(files=args, force=opts.force)
-
- elif opts.action == 'move':
- if len(args) > 1:
- print("Error: Move cannot handle multiple targets.")
- exit(-1)
- repo.move(args[0])
-
- else:
- print("Error: Something truly terrible has happened.")
- exit(-1)
-
-
-def check_repository_exists():
- if not os.path.exists(repo_settings['path']):
- print('Error: Could not find dotfiles repository \"%s\"' % (
- repo_settings['path']))
- if compare_path(repo_settings['path'], Repository.defaults['path']):
- missing_default_repo()
- exit(-1)
-
-
-def update_settings(opts, key):
- global repo_settings
-
- value = opts.get(key)
- if value:
- repo_settings[key].update(value)
-
-
-def main():
-
- global repo_settings
-
- (cli_opts, args) = parse_args()
-
- repo_settings['homedir'] = realpath_expanduser(
- cli_opts.homedir or repo_settings['homedir'])
-
- config_opts = parse_config(cli_opts.config_file or
- realpath_expanduser('~/%s' % CONFIG_FILE))
-
- repo_settings['path'] = realpath_expanduser(
- cli_opts.repository or
- config_opts.get('repository') or
- repo_settings['path'])
-
- check_repository_exists()
-
- update_settings(config_opts, 'ignore')
- update_settings(config_opts, 'externals')
- update_settings(config_opts, 'packages')
-
- repo_config_file = os.path.join(repo_settings['path'], CONFIG_FILE)
- repo_config_opts = parse_config(repo_config_file)
-
- repo_settings['prefix'] = (cli_opts.prefix or
- repo_config_opts.get('prefix') or
- config_opts.get('prefix') or
- repo_settings['prefix'])
- repo_settings['no_dot_prefix'] = cli_opts.no_dot_prefix
-
- update_settings(repo_config_opts, 'ignore')
- update_settings(repo_config_opts, 'externals')
- update_settings(repo_config_opts, 'packages')
-
- repo = Repository(**repo_settings)
-
- dispatch(repo, cli_opts, args)
diff --git a/dotfiles/compat.py b/dotfiles/compat.py
deleted file mode 100644
index 456b9c9..0000000
--- a/dotfiles/compat.py
+++ /dev/null
@@ -1,214 +0,0 @@
-"""
-Provides :func:`os.symlink`, :func:`os.path.islink` and
-:func:`os.path.realpath` implementations for win32.
-"""
-
-import os
-import os.path
-
-
-def is_unicode(s):
- return type(s) == unicode # noqa
-
-if hasattr(os, 'symlink'):
- symlink = os.symlink
- islink = os.path.islink
- realpath = os.path.realpath
-else:
- # Windows symlinks -- ctypes version
- # symlink, islink, readlink, realpath, is_link_to
-
- win32_verbose = False # set to True to debug symlink stuff
- import os
- import ctypes
- import struct
- from ctypes import windll, wintypes
-
- FSCTL_GET_REPARSE_POINT = 0x900a8
-
- FILE_ATTRIBUTE_READONLY = 0x0001
- FILE_ATTRIBUTE_HIDDEN = 0x0002
- FILE_ATTRIBUTE_DIRECTORY = 0x0010
- FILE_ATTRIBUTE_NORMAL = 0x0080
- FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
-
- GENERIC_READ = 0x80000000
- GENERIC_WRITE = 0x40000000
- OPEN_EXISTING = 3
- FILE_READ_ATTRIBUTES = 0x80
- FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
- INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
-
- INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
-
- FILE_FLAG_OPEN_REPARSE_POINT = 2097152
- FILE_FLAG_BACKUP_SEMANTICS = 33554432
- # FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTI
- FILE_FLAG_REPARSE_BACKUP = 35651584
-
- kdll = windll.LoadLibrary("kernel32.dll")
- CreateSymbolicLinkA = windll.kernel32.CreateSymbolicLinkA
- CreateSymbolicLinkA.restype = wintypes.BOOLEAN
- CreateSymbolicLinkW = windll.kernel32.CreateSymbolicLinkW
- CreateSymbolicLinkW.restype = wintypes.BOOLEAN
- GetFileAttributesA = windll.kernel32.GetFileAttributesA
- GetFileAttributesW = windll.kernel32.GetFileAttributesW
- CloseHandle = windll.kernel32.CloseHandle
- _CreateFileW = windll.kernel32.CreateFileW
- _CreateFileA = windll.kernel32.CreateFileA
- _DevIoCtl = windll.kernel32.DeviceIoControl
- _DevIoCtl.argtypes = [
- wintypes.HANDLE, # HANDLE hDevice
- wintypes.DWORD, # DWORD dwIoControlCode
- wintypes.LPVOID, # LPVOID lpInBuffer
- wintypes.DWORD, # DWORD nInBufferSize
- wintypes.LPVOID, # LPVOID lpOutBuffer
- wintypes.DWORD, # DWORD nOutBufferSize
- ctypes.POINTER(wintypes.DWORD), # LPDWORD lpBytesReturned
- wintypes.LPVOID] # LPOVERLAPPED lpOverlapped
- _DevIoCtl.restype = wintypes.BOOL
-
- def CreateSymbolicLink(name, target, is_dir):
- assert type(name) == type(target)
- if is_unicode(name):
- stat = CreateSymbolicLinkW(name, target, is_dir)
- else:
- stat = CreateSymbolicLinkA(name, target, is_dir)
- if win32_verbose:
- print("CreateSymbolicLink(name=%s, target=%s, is_dir=%d) = %#x" %
- (name, target, is_dir, stat))
- if not stat:
- print("Can't create symlink %s -> %s" % (name, target))
- raise ctypes.WinError()
-
- def symlink(target, name):
- CreateSymbolicLink(name, target, 0)
-
- def GetFileAttributes(path):
- if is_unicode(path):
- return GetFileAttributesW(path)
- else:
- return GetFileAttributesA(path)
-
- def islink(path):
- assert path
- has_link_attr = GetFileAttributes(path) & FILE_ATTRIBUTE_REPARSE_POINT
- if win32_verbose:
- print("islink(%s): attrs=%#x: %s" %
- (path, GetFileAttributes(path), has_link_attr != 0))
- return has_link_attr != 0
-
- def DeviceIoControl(hDevice, ioControlCode, input, output):
- # DeviceIoControl Function
- # http://msdn.microsoft.com/en-us/library/aa363216(v=vs.85).aspx
- if input:
- input_size = len(input)
- else:
- input_size = 0
- if isinstance(output, int):
- output = ctypes.create_string_buffer(output)
- output_size = len(output)
- assert isinstance(output, ctypes.Array)
- bytesReturned = wintypes.DWORD()
- status = _DevIoCtl(hDevice, ioControlCode, input, input_size, output,
- output_size, bytesReturned, None)
- if win32_verbose:
- print("DeviceIOControl: status = %d" % status)
- if status != 0:
- return output[:bytesReturned.value]
- else:
- return None
-
- def CreateFile(path, access, sharemode, creation, flags):
- if is_unicode(path):
- return _CreateFileW(path, access, sharemode, None, creation,
- flags, None)
- else:
- return _CreateFileA(path, access, sharemode, None, creation,
- flags, None)
-
- SymbolicLinkReparseFormat = "LHHHHHHL"
- SymbolicLinkReparseSize = struct.calcsize(SymbolicLinkReparseFormat)
-
- def readlink(path):
- """ Windows readlink implementation. """
- # This wouldn't return true if the file didn't exist, as far as I know.
- if not islink(path):
- if win32_verbose:
- print("readlink(%s): not a link." % path)
- return None
-
- # Open the file correctly depending on the string type.
- hfile = CreateFile(path, GENERIC_READ, 0, OPEN_EXISTING,
- FILE_FLAG_OPEN_REPARSE_POINT)
-
- # MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 = (16*1024)
- buffer = DeviceIoControl(hfile, FSCTL_GET_REPARSE_POINT, None, 16384)
- CloseHandle(hfile)
-
- # Minimum possible length (assuming length of target is bigger than 0)
- if not buffer or len(buffer) < 9:
- if win32_verbose:
- print("readlink(%s): no reparse buffer." % path)
- return None
-
- # Parse and return our result.
- # typedef struct _REPARSE_DATA_BUFFER {
- # ULONG ReparseTag;
- # USHORT ReparseDataLength;
- # USHORT Reserved;
- # union {
- # struct {
- # USHORT SubstituteNameOffset;
- # USHORT SubstituteNameLength;
- # USHORT PrintNameOffset;
- # USHORT PrintNameLength;
- # ULONG Flags;
- # WCHAR PathBuffer[1];
- # } SymbolicLinkReparseBuffer;
- # struct {
- # USHORT SubstituteNameOffset;
- # USHORT SubstituteNameLength;
- # USHORT PrintNameOffset;
- # USHORT PrintNameLength;
- # WCHAR PathBuffer[1];
- # } MountPointReparseBuffer;
- # struct {
- # UCHAR DataBuffer[1];
- # } GenericReparseBuffer;
- # } DUMMYUNIONNAME;
- # } REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
-
- # Only handle SymbolicLinkReparseBuffer
- (tag, dataLength, reserver, SubstituteNameOffset, SubstituteNameLength,
- PrintNameOffset, PrintNameLength,
- Flags) = struct.unpack(SymbolicLinkReparseFormat,
- buffer[:SymbolicLinkReparseSize])
- # print(tag, dataLength, reserver, SubstituteNameOffset,
- # iSubstituteNameLength)
- start = SubstituteNameOffset + SymbolicLinkReparseSize
- actualPath = buffer[start:
- start + SubstituteNameLength].decode("utf-16")
- # This utf-16 string is null terminated
- index = actualPath.find("\0")
- if index > 0:
- actualPath = actualPath[:index]
- if actualPath.startswith("\\??\\"): # ASCII 92, 63, 63, 92
- ret = actualPath[4:] # strip off leading junk
- else:
- ret = actualPath
- if win32_verbose:
- print("readlink(%s->%s->%s): index(null) = %d" %
- (path, repr(actualPath), repr(ret), index))
- return ret
-
- def realpath(fpath):
- while islink(fpath):
- rpath = readlink(fpath)
- if rpath is None:
- return fpath
- if not os.path.isabs(rpath):
- rpath = os.path.abspath(os.path.join(os.path.dirname(fpath),
- rpath))
- fpath = rpath
- return fpath
diff --git a/dotfiles/core.py b/dotfiles/core.py
deleted file mode 100644
index 138332d..0000000
--- a/dotfiles/core.py
+++ /dev/null
@@ -1,269 +0,0 @@
-import os
-import os.path
-import shutil
-import fnmatch
-
-from .utils import realpath_expanduser, is_link_to
-from .compat import symlink
-
-
-class Dotfile(object):
-
- def __init__(self, name, target, home, add_dot=True, dry_run=False):
- if name.startswith('/'):
- self.name = name
- else:
- if add_dot:
- self.name = os.path.join(home, '.%s' % name.strip('.'))
- else:
- self.name = os.path.join(home, name)
- self.basename = os.path.basename(self.name)
- self.target = target.rstrip('/')
- self.dry_run = dry_run
- self.status = ''
- if not os.path.lexists(self.name):
- self.status = 'missing'
- elif not is_link_to(self.name, self.target):
- self.status = 'unsynced'
-
- def _symlink(self, target, name):
- if not self.dry_run:
- dirname = os.path.dirname(name)
- if not os.path.isdir(dirname):
- os.makedirs(dirname)
- symlink(target, name)
- else:
- print("Creating symlink %s => %s" % (target, name))
-
- def _rmtree(self, path):
- if not self.dry_run:
- shutil.rmtree(path)
- else:
- print("Removing %s and everything under it" % path)
-
- def _remove(self, path):
- if not self.dry_run:
- os.remove(path)
- else:
- print("Removing %s" % path)
-
- def _move(self, src, dst):
- if not self.dry_run:
- shutil.move(src, dst)
- else:
- print("Moving %s => %s" % (src, dst))
-
- def sync(self, force):
- if self.status == 'missing':
- self._symlink(self.target, self.name)
- elif self.status == 'unsynced':
- if not force:
- print("Skipping \"%s\", use --force to override"
- % self.basename)
- return
- if os.path.isdir(self.name) and not os.path.islink(self.name):
- self._rmtree(self.name)
- else:
- self._remove(self.name)
- self._symlink(self.target, self.name)
-
- def add(self):
- if self.status == 'missing':
- print("Skipping \"%s\", file not found" % self.basename)
- return
- if self.status == '':
- print("Skipping \"%s\", already managed" % self.basename)
- return
- self._move(self.name, self.target)
- self._symlink(self.target, self.name)
-
- def remove(self):
-
- if self.status != '':
- print("Skipping \"%s\", file is %s" % (self.basename, self.status))
- return
-
- # remove the existing symlink
- self._remove(self.name)
-
- # return dotfile to its original location
- if os.path.exists(self.target):
- self._move(self.target, self.name)
-
- def __str__(self):
- user_home = os.environ['HOME']
- common_prefix = os.path.commonprefix([user_home, self.name])
- if common_prefix:
- name = '~%s' % self.name[len(common_prefix):]
- else:
- name = self.name
- return '%-18s %-s' % (name, self.status)
-
-
-class Dotfiles(object):
- """A Dotfiles Repository."""
-
- defaults = {
- 'prefix': '',
- 'packages': set(),
- 'externals': dict(),
- 'ignore': set(['.dotfilesrc']),
- 'homedir': os.path.expanduser('~/'),
- 'path': os.path.expanduser('~/Dotfiles'),
- 'no_dot_prefix': False
- }
-
- def __init__(self, **kwargs):
-
- # merge provided arguments with defaults into configuration
- configuration = {key: kwargs.get(key, self.defaults[key])
- for key in self.defaults}
-
- # map configuration items to instance-local variables
- for k, v in configuration.items():
- setattr(self, k, v)
-
- # FIXME: compatibility shims, remove these
- self.dry_run = False
- self.repository = self.path
-
- self._load()
-
- def _load(self):
- """Load each dotfile in the repository."""
-
- self.dotfiles = list()
- self._load_recursive()
-
- def _load_recursive(self, sub_dir=''):
- """Recursive helper for :meth:`_load`."""
-
- src_dir = os.path.join(self.repository, sub_dir)
- if sub_dir and not self.prefix:
- # Add a dot to first level of packages
- dst_dir = os.path.join(self.homedir, '.%s' % sub_dir)
- else:
- dst_dir = os.path.join(self.homedir, sub_dir)
-
- all_repofiles = os.listdir(src_dir)
- repofiles_to_symlink = set(all_repofiles)
-
- for pat in self.ignore:
- repofiles_to_symlink.difference_update(
- fnmatch.filter(all_repofiles, pat))
-
- for dotfile in repofiles_to_symlink:
- pkg_path = os.path.join(sub_dir, dotfile)
- if pkg_path in self.packages:
- self._load_recursive(pkg_path)
- else:
- add_dot = False if self.no_dot_prefix else not bool(sub_dir)
- self.dotfiles.append(Dotfile(dotfile,
- os.path.join(src_dir, dotfile),
- dst_dir, add_dot=add_dot,
- dry_run=self.dry_run))
-
- # Externals are top-level only
- if not sub_dir:
- for dotfile in self.externals.keys():
- self.dotfiles.append(
- Dotfile(dotfile,
- os.path.expanduser(self.externals[dotfile]),
- dst_dir, add_dot=not bool(sub_dir),
- dry_run=self.dry_run))
-
- def _fqpn(self, dotfile, pkg_name=None):
- """Return the fully qualified path to a dotfile."""
- if pkg_name is None:
- return os.path.join(self.repository,
- self.prefix +
- os.path.basename(dotfile).strip('.'))
- return os.path.join(self.repository, self.prefix + pkg_name,
- os.path.basename(dotfile))
-
- def list(self, verbose=True):
- """List the contents of this repository."""
-
- for dotfile in sorted(self.dotfiles, key=lambda dotfile: dotfile.name):
- if dotfile.status or verbose:
- print(dotfile)
-
- def check(self):
- """List only unsynced and/or missing dotfiles."""
-
- self.list(verbose=False)
-
- def sync(self, files=None, force=False):
- """
- Synchronize this repository, creating and updating the necessary
- symbolic links.
- """
-
- # unless a set of files is specified, operate on all files
- if not files:
- dotfiles = self.dotfiles
- else:
- files = set(map(lambda x: os.path.join(self.homedir, x), files))
- dotfiles = [x for x in self.dotfiles if x.name in files]
- if not dotfiles:
- raise Exception("file not found")
-
- for dotfile in dotfiles:
- dotfile.sync(force)
-
- 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:
- file = file.rstrip('/')
- # See if file is inside a package
- file_dir, file_name = os.path.split(file)
- common_prefix = os.path.commonprefix([self.homedir, file_dir])
- sub_dir = file_dir[len(common_prefix) + 1:]
- pkg_name = sub_dir.lstrip('.')
- if pkg_name in self.packages:
- home = os.path.join(self.homedir, sub_dir)
- target = self._fqpn(file, pkg_name=pkg_name)
- dirname = os.path.dirname(target)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
- else:
- home = self.homedir
- target = self._fqpn(file)
- if (action == 'add' and
- os.path.split(target)[1] in self.packages):
- print("Skipping \"%s\", packages not yet supported" % file)
- return
- if sub_dir.startswith('.') or file_name.startswith('.'):
- dotfile = Dotfile(file, target, home, dry_run=self.dry_run)
- getattr(dotfile, action)()
- else:
- print("Skipping \"%s\", not a dotfile" % file)
-
- def move(self, target):
- """Move the repository to another location."""
- target = realpath_expanduser(target)
-
- if os.path.exists(target):
- raise ValueError('Target already exists: %s' % (target))
-
- if not self.dry_run:
- shutil.copytree(self.repository, target, symlinks=True)
- shutil.rmtree(self.repository)
- else:
- print("Recursive copy %s => %s" % (self.repository, target))
- print("Removing %s and everything under it" % self.repository)
-
- self.repository = target
-
- if not self.dry_run:
- self._load()
- self.sync(force=True)
diff --git a/dotfiles/dotfile.py b/dotfiles/dotfile.py
deleted file mode 100644
index ddb2bb2..0000000
--- a/dotfiles/dotfile.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import errno
-
-
-def unique_suffix(path_a, path_b):
- discard = len(str(path_a.common(path_b))) + 1
- return (str(path_a)[discard:], str(path_b)[discard:])
-
-
-class Dotfile(object):
- """
- This class implements the 'dotfile' abstraction.
-
- A dotfile has two primary attributes:
-
- name -- name of symlink in the home directory (~/.vimrc)
- target -- where the symlink should point to (~/Dotfiles/vimrc)
-
- The above attributes are both py.path.local objects.
-
- The goal is for there to be no special logic or stored global state. Only
- the implementation of three operations made available to the caller:
-
- add -- move a dotfile into the repository and replace it with a symlink
- remove -- the opposite of add
- sync -- ensure that each repository file has a corresponding symlink
- unsync -- remove the symlink leaving only the repository file
-
- This is where most filesystem operations (link, delete, etc) should be
- called, and not in the layers above.
- """
-
- def __init__(self, name, target):
- self.name = name
- self.target = target
-
- def __str__(self):
- short_name, _ = unique_suffix(self.name, self.target)
- return '%s' % short_name
-
- def __repr__(self):
- return '<Dotfile %r>' % self.name
-
- @property
- def state(self):
-
- # lets be optimistic
- state = 'ok'
-
- if self.target.check(exists=0):
- # only for testing, cli should never reach this state
- state = 'error'
- elif self.name.check(exists=0):
- # no $HOME symlink
- state = 'missing'
- elif self.name.check(link=0) or not self.name.samefile(self.target):
- # if name exists but isn't a link to the target
- state = 'conflict'
-
- return state
-
- def add(self):
- if self.target.check(exists=1):
- raise OSError(errno.EEXIST, self.target)
- self.name.move(self.target)
- self.sync()
-
- def remove(self):
- if self.target.check(exists=0):
- raise OSError(errno.ENOENT, self.target)
- self.name.remove()
- self.target.move(self.name)
-
- def sync(self):
- self.name.mksymlinkto(self.target)
-
- def unsync(self):
- self.name.remove()
diff --git a/dotfiles/repository.py b/dotfiles/repository.py
deleted file mode 100644
index d9750ef..0000000
--- a/dotfiles/repository.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import py
-from operator import attrgetter
-
-from .dotfile import Dotfile
-
-
-class Repository(object):
- """
- This class implements the 'repository' abstraction.
-
- A repository is a directory that contains dotfiles. It has two primary
- attributes:
-
- repodir -- the location of the repository directory
- homedir -- the location of the home directory (primarily for testing)
-
- Both of the above attributes are received as string objects and converted
- to py.path.local objects for internal use.
- """
-
- def __init__(self, repodir, homedir='~'):
- self.repodir = repodir
- self.homedir = homedir
-
- def __str__(self):
- """Convert repository contents to human readable form."""
- return ''.join('%s\n' % item for item in self.contents()).rstrip()
-
- def __repr__(self):
- return '<Repository %r>' % self.repodir
-
- @property
- def repodir(self):
- return str(self._repodir)
-
- @repodir.setter
- def repodir(self, path):
- self._repodir = py.path.local(path, expanduser=True)
-
- @property
- def homedir(self):
- return str(self._homedir)
-
- @homedir.setter
- def homedir(self, path):
- self._homedir = py.path.local(path, expanduser=True)
-
- def _expected_name(self, target):
- return py.path.local("%s/.%s" % (self.homedir, target.basename))
-
- def contents(self):
- """Given a repository path, discover all existing dotfiles."""
- contents = []
- self._repodir.ensure(dir=1)
- for target in self._repodir.listdir():
- target = py.path.local(target)
- contents.append(Dotfile(self._expected_name(target), target))
- return sorted(contents, key=attrgetter('name'))
-
- def rename(self):
- raise NotImplementedError
diff --git a/dotfiles/utils.py b/dotfiles/utils.py
deleted file mode 100644
index fd5909f..0000000
--- a/dotfiles/utils.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-Misc utility functions.
-"""
-
-import os.path
-
-from .compat import islink, realpath
-
-
-def compare_path(path1, path2):
- return (realpath_expanduser(path1) == realpath_expanduser(path2))
-
-
-def realpath_expanduser(path):
- return realpath(os.path.expanduser(path))
-
-
-def is_link_to(path, target):
- def normalize(path):
- return os.path.normcase(os.path.normpath(path))
- return islink(path) and \
- normalize(realpath(path)) == normalize(realpath(target))
diff --git a/setup.py b/setup.py
index 8e0cc63..9d79bbd 100644
--- a/setup.py
+++ b/setup.py
@@ -13,19 +13,21 @@ setup(
open('LICENSE.rst').read() + '\n\n' +
open('HISTORY.rst').read()),
license='ISC',
- packages=['dotfiles'],
+ py_modules=['dotifles'],
install_requires=[
'click',
'py',
],
entry_points={
'console_scripts': [
- 'dotfiles=dotfiles.cli_orig:main',
+ 'dotfiles=dotfiles:cli',
],
},
classifiers=[
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
'License :: OSI Approved :: ISC License (ISCL)'
],
)