summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-11-15 16:25:57 +0100
committerAnton Khirnov <anton@khirnov.net>2020-11-15 16:25:57 +0100
commit79c9c0747e876c0b97a4fb3fe91fa9825e462722 (patch)
treeff298dd32dfccfde9921d0ac2c48bc36113a55c6
parent5ffd208bdb85215cc7c5ba6f2903bfa7756aa945 (diff)
Add a dry run mode.
-rw-r--r--dotbot/cli.py5
-rw-r--r--dotbot/dispatcher.py5
-rw-r--r--dotbot/plugin.py5
-rw-r--r--dotbot/plugins/clean.py33
-rw-r--r--dotbot/plugins/create.py34
-rw-r--r--dotbot/plugins/link.py50
-rw-r--r--dotbot/plugins/shell.py13
7 files changed, 98 insertions, 47 deletions
diff --git a/dotbot/cli.py b/dotbot/cli.py
index 0795d40..b09461e 100644
--- a/dotbot/cli.py
+++ b/dotbot/cli.py
@@ -18,6 +18,8 @@ def add_options(parser):
help='suppress most output')
parser.add_argument('-v', '--verbose', action='store_true',
help='enable verbose output')
+ parser.add_argument('-n', '--dry-run', action='store_true',
+ help='do not actually perform any changes')
parser.add_argument('-d', '--base-directory',
help='execute commands from within BASEDIR',
metavar='BASEDIR')
@@ -97,7 +99,8 @@ def main():
# default to directory of config file
base_directory = os.path.dirname(os.path.abspath(options.config_file))
os.chdir(base_directory)
- dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip)
+ dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip,
+ dry_run = options.dry_run)
success = dispatcher.dispatch(tasks)
if success:
log.verbose('\n==> All tasks executed successfully')
diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py
index f809bef..9c55050 100644
--- a/dotbot/dispatcher.py
+++ b/dotbot/dispatcher.py
@@ -4,12 +4,13 @@ from .messenger import Messenger
from .context import Context
class Dispatcher(object):
- def __init__(self, base_directory, only=None, skip=None):
+ def __init__(self, base_directory, only=None, skip=None, dry_run = False):
self._log = Messenger()
self._setup_context(base_directory)
self._load_plugins()
self._only = only
self._skip = skip
+ self._dry_run = dry_run
def _setup_context(self, base_directory):
path = os.path.abspath(
@@ -34,7 +35,7 @@ class Dispatcher(object):
for plugin in self._plugins:
if plugin.can_handle(action):
try:
- success &= plugin.handle(action, task[action])
+ success &= plugin.handle(action, task[action], self._dry_run)
handled = True
except Exception as err:
self._log.error(
diff --git a/dotbot/plugin.py b/dotbot/plugin.py
index 5e2c923..2d12636 100644
--- a/dotbot/plugin.py
+++ b/dotbot/plugin.py
@@ -18,10 +18,13 @@ class Plugin(object):
'''
return directive == self._directive
- def handle(self, directive, data):
+ def handle(self, directive, data, dry_run = False):
'''
Executes the directive.
Returns true if the Plugin successfully handled the directive.
+
+ When dry_run is True, the plugin will not perform any actions, only
+ return False when there is nothing to do, True otherwise.
'''
raise NotImplementedError
diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py
index c8f3a03..212fb07 100644
--- a/dotbot/plugins/clean.py
+++ b/dotbot/plugins/clean.py
@@ -9,7 +9,7 @@ class Clean(dotbot.Plugin):
_directive = 'clean'
- def handle(self, directive, targets):
+ def handle(self, directive, targets, dry_run):
success = True
defaults = self._context.defaults().get(self._directive, {})
for target in targets:
@@ -18,14 +18,22 @@ class Clean(dotbot.Plugin):
if isinstance(targets, dict) and isinstance(targets[target], dict):
force = targets[target].get('force', force)
recursive = targets[target].get('recursive', recursive)
- success &= self._clean(target, force, recursive)
- if success:
- self._log.verbose('All targets have been cleaned')
+ success &= self._clean(target, force, recurseve, dry_run)
+
+ if dry_run:
+ if success:
+ self._log.verbose('clean/dry run: nothing to do')
+ else:
+ self._log.verbose('clean/dry run: some targets are dirty')
else:
- self._log.error('Some targets were not successfully cleaned')
+ if success:
+ self._log.verbose('All targets have been cleaned')
+ else:
+ self._log.error('Some targets were not successfully cleaned')
+
return success
- def _clean(self, target, force, recursive):
+ def _clean(self, target, force, recursive, dry_run):
'''
Cleans all the broken symbolic links in target if they point to
a subdirectory of the base directory or if forced to clean.
@@ -33,21 +41,28 @@ class Clean(dotbot.Plugin):
if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))):
self._log.debug('Ignoring nonexistent directory %s' % target)
return True
+
+ ret = True
for item in os.listdir(os.path.expandvars(os.path.expanduser(target))):
path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item)
if recursive and os.path.isdir(path):
# isdir implies not islink -- we don't want to descend into
# symlinked directories. okay to do a recursive call here
# because depth should be fairly limited
- self._clean(path, force, recursive)
+ ret &= self._clean(path, force, recursive)
if not os.path.exists(path) and os.path.islink(path):
points_at = os.path.join(os.path.dirname(path), os.readlink(path))
if self._in_directory(path, self._context.base_directory()) or force:
self._log.info('Removing invalid link %s -> %s' % (path, points_at))
- os.remove(path)
+
+ if dry_run:
+ ret = False
+ else:
+ os.remove(path)
else:
self._log.verbose('Link %s -> %s not removed.' % (path, points_at))
- return True
+
+ return ret
def _in_directory(self, path, directory):
'''
diff --git a/dotbot/plugins/create.py b/dotbot/plugins/create.py
index 2420558..aa14277 100644
--- a/dotbot/plugins/create.py
+++ b/dotbot/plugins/create.py
@@ -9,15 +9,23 @@ class Create(dotbot.Plugin):
_directive = 'create'
- def handle(self, directive, paths):
+ def handle(self, directive, paths, dry_run):
success = True
for path in paths:
path = os.path.expandvars(os.path.expanduser(path))
- success &= self._create(path)
- if success:
- self._log.verbose('All paths have been set up')
+ success &= self._create(path, dry_run)
+
+ if dry_run:
+ if success:
+ self._log.verbose('create/dry run: nothing to do')
+ else:
+ self._log.verbose('create/dry run: some targets are missing')
else:
- self._log.error('Some paths were not successfully set up')
+ if success:
+ self._log.verbose('All paths have been set up')
+ else:
+ self._log.error('Some paths were not successfully set up')
+
return success
def _exists(self, path):
@@ -27,16 +35,20 @@ class Create(dotbot.Plugin):
path = os.path.expanduser(path)
return os.path.exists(path)
- def _create(self, path):
+ def _create(self, path, dry_run):
+ if self._exists(path):
+ self._log.verbose('Path exists %s' % path)
+ return True
+
success = True
- if not self._exists(path):
- self._log.debug('Trying to create path %s' % path)
+ self._log.info('Creating path %s' % path)
+ if dry_run:
+ success = False
+ else:
try:
- self._log.info('Creating path %s' % path)
os.makedirs(path)
except OSError:
self._log.warning('Failed to create path %s' % path)
success = False
- else:
- self._log.verbose('Path exists %s' % path)
+
return success
diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py
index 411a0ef..5e2bd90 100644
--- a/dotbot/plugins/link.py
+++ b/dotbot/plugins/link.py
@@ -11,7 +11,7 @@ class Link(dotbot.Plugin):
_directive = 'link'
- def handle(self, directive, links):
+ def handle(self, directive, links, dry_run):
success = True
defaults = self._context.defaults().get('link', {})
for destination, source in links.items():
@@ -47,12 +47,20 @@ class Link(dotbot.Plugin):
(destination, path))
continue
if force or relink:
- success &= self._delete(path, destination, relative, canonical_path, force)
- success &= self._link(path, destination, relative, canonical_path, ignore_missing)
- if success:
- self._log.verbose('All links have been set up')
+ success &= self._delete(path, destination, relative, canonical_path, force, dry_run)
+ success &= self._link(path, destination, relative, canonical_path, ignore_missing, dry_run)
+
+ if dry_run:
+ if success:
+ self._log.verbose('link/dry run: nothing to do')
+ else:
+ self._log.verbose('link/dry run: some targets are missing')
else:
- self._log.error('Some links were not successfully set up')
+ if success:
+ self._log.verbose('All links have been set up')
+ else:
+ self._log.error('Some links were not successfully set up')
+
return success
def _test_success(self, command):
@@ -91,7 +99,7 @@ class Link(dotbot.Plugin):
path = os.path.expanduser(path)
return os.path.exists(path)
- def _delete(self, source, path, relative, canonical_path, force):
+ def _delete(self, source, path, relative, canonical_path, force, dry_run):
success = True
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
fullpath = os.path.expanduser(path)
@@ -99,7 +107,11 @@ class Link(dotbot.Plugin):
source = self._relative_path(source, fullpath)
if ((self._is_link(path) and self._link_destination(path) != source) or
(self._exists(path) and not self._is_link(path))):
- removed = False
+ self._log.info('Removing %s' % path)
+
+ if dry_run:
+ return False
+
try:
if os.path.islink(fullpath):
os.unlink(fullpath)
@@ -107,16 +119,12 @@ class Link(dotbot.Plugin):
elif force:
if os.path.isdir(fullpath):
shutil.rmtree(fullpath)
- removed = True
else:
os.remove(fullpath)
- removed = True
except OSError:
self._log.warning('Failed to remove %s' % path)
success = False
- else:
- if removed:
- self._log.info('Removing %s' % path)
+
return success
def _relative_path(self, source, destination):
@@ -127,7 +135,7 @@ class Link(dotbot.Plugin):
destination_dir = os.path.dirname(destination)
return os.path.relpath(source, destination_dir)
- def _link(self, source, link_name, relative, canonical_path, ignore_missing):
+ def _link(self, source, link_name, relative, canonical_path, ignore_missing, dry_run):
'''
Links link_name to source.
@@ -149,13 +157,13 @@ class Link(dotbot.Plugin):
# directory, and if source is relative, it will be relative to the
# destination directory
elif not self._exists(link_name) and (ignore_missing or self._exists(absolute_source)):
- try:
- os.symlink(source, destination)
- except OSError:
- self._log.warning('Linking failed %s -> %s' % (link_name, source))
- else:
- self._log.verbose('Creating link %s -> %s' % (link_name, source))
- success = True
+ self._log.verbose('Creating link %s -> %s' % (link_name, source))
+ if not dry_run:
+ try:
+ os.symlink(source, destination)
+ success = True
+ except OSError:
+ self._log.warning('Linking failed %s -> %s' % (link_name, source))
elif self._exists(link_name) and not self._is_link(link_name):
self._log.warning(
'%s already exists but is a regular file or directory' %
diff --git a/dotbot/plugins/shell.py b/dotbot/plugins/shell.py
index f0121e6..75d4dca 100644
--- a/dotbot/plugins/shell.py
+++ b/dotbot/plugins/shell.py
@@ -11,7 +11,7 @@ class Shell(dotbot.Plugin):
_directive = 'shell'
- def handle(self, directive, data):
+ def handle(self, directive, data, dry_run):
success = True
defaults = self._context.defaults().get('shell', {})
for item in data:
@@ -38,6 +38,10 @@ class Shell(dotbot.Plugin):
self._log.info('%s' % msg)
else:
self._log.info('%s [%s]' % (msg, cmd))
+
+ if dry_run:
+ continue
+
ret = dotbot.util.shell_command(
cmd,
cwd=self._context.base_directory(),
@@ -48,8 +52,13 @@ class Shell(dotbot.Plugin):
if ret != 0:
success = False
self._log.warning('Command [%s] failed' % cmd)
- if success:
+
+ if dry_run:
+ self._log.verbose('shell/dry run: commands not executed')
+ success = False
+ elif success:
self._log.verbose('All commands have been executed')
else:
self._log.error('Some commands were not successfully executed')
+
return success