mirror of
https://github.com/deadc0de6/dotdrop.git
synced 2026-02-04 23:14:47 +00:00
feature for #458
This commit is contained in:
24
docs/config/config-dotfiles.md
vendored
24
docs/config/config-dotfiles.md
vendored
@@ -10,6 +10,7 @@ Entry | Description
|
||||
`actions` | List of action keys that need to be defined in the **actions** entry below (See [actions](config-actions.md))
|
||||
`chmod` | Defines the file permissions in octal notation to apply during installation or the special keyword `preserve` (See [permissions](config-file.md#permissions))
|
||||
`cmpignore` | List of patterns to ignore when comparing (enclose in quotes when using wildcards; see [ignore patterns](config-file.md#ignore-patterns))
|
||||
`handle_dir_as_block` | When true, directories are handled as a single block during update operations instead of processing each file individually (defaults to false)
|
||||
`ignore_missing_in_dotdrop` | Ignore missing files in dotdrop when comparing and importing (see [Ignore missing](config-file.md#ignore-missing))
|
||||
`ignoreempty` | If true, an empty template will not be deployed (defaults to the value of `ignoreempty`)
|
||||
`instignore` | List of patterns to ignore when installing (enclose in quotes when using wildcards; see [ignore patterns](config-file.md#ignore-patterns))
|
||||
@@ -216,4 +217,25 @@ profiles:
|
||||
- f_test
|
||||
```
|
||||
|
||||
Make sure to quote the link value in the config file.
|
||||
Make sure to quote the link value in the config file.
|
||||
|
||||
## Handle directories as blocks
|
||||
|
||||
When managing dotfiles that are directories, dotdrop normally processes each file and subdirectory individually. This allows for precise control over the contents, showing individual file differences, and selectively updating files.
|
||||
However, in some cases, you may prefer to treat an entire directory as a single unit.
|
||||
|
||||
For these scenarios, you can use the `handle_dir_as_block` option on specific dotfiles:
|
||||
|
||||
```yaml
|
||||
dotfiles:
|
||||
d_config:
|
||||
src: app
|
||||
dst: ~/.config/app
|
||||
handle_dir_as_block: true
|
||||
```
|
||||
|
||||
When this option is enabled:
|
||||
- During **install** operations, the entire directory will be replaced as a whole, rather than updating individual files
|
||||
- This option has **no effect** on **compare** operations, which will always show file-by-file differences
|
||||
|
||||
This option defaults to `false` and can be set on any dotfile that represents a directory. It has no effect on dotfiles that are regular files.
|
||||
@@ -232,12 +232,20 @@ def _dotfile_install(opts, dotfile, tmpdir=None):
|
||||
LinkTypes.RELATIVE, LinkTypes.ABSOLUTE
|
||||
):
|
||||
# nolink|relative|absolute|link_children
|
||||
ret, err = inst.install(templ, dotfile.src, dotfile.dst,
|
||||
dotfile.link,
|
||||
actionexec=pre_actions_exec,
|
||||
is_template=is_template,
|
||||
ignore=ignores,
|
||||
chmod=dotfile.chmod)
|
||||
asblock = False
|
||||
if hasattr(dotfile, 'handle_dir_as_block'):
|
||||
asblock = True
|
||||
ret, err = inst.install(
|
||||
templ,
|
||||
dotfile.src,
|
||||
dotfile.dst,
|
||||
dotfile.link,
|
||||
actionexec=pre_actions_exec,
|
||||
is_template=is_template,
|
||||
ignore=ignores,
|
||||
chmod=dotfile.chmod,
|
||||
handle_dir_as_block=asblock,
|
||||
)
|
||||
else:
|
||||
# nolink
|
||||
src = dotfile.src
|
||||
@@ -250,13 +258,21 @@ def _dotfile_install(opts, dotfile, tmpdir=None):
|
||||
src = tmp
|
||||
# make sure to re-evaluate if is template
|
||||
is_template = dotfile.template and Templategen.path_is_template(src)
|
||||
ret, err = inst.install(templ, src, dotfile.dst,
|
||||
LinkTypes.NOLINK,
|
||||
actionexec=pre_actions_exec,
|
||||
noempty=dotfile.noempty,
|
||||
ignore=ignores,
|
||||
is_template=is_template,
|
||||
chmod=dotfile.chmod)
|
||||
asblock = False
|
||||
if hasattr(dotfile, "handle_dir_as_block"):
|
||||
asblock = True
|
||||
ret, err = inst.install(
|
||||
templ,
|
||||
src,
|
||||
dotfile.dst,
|
||||
LinkTypes.NOLINK,
|
||||
actionexec=pre_actions_exec,
|
||||
noempty=dotfile.noempty,
|
||||
ignore=ignores,
|
||||
is_template=is_template,
|
||||
chmod=dotfile.chmod,
|
||||
handle_dir_as_block=asblock,
|
||||
)
|
||||
if tmp:
|
||||
tmp = os.path.join(opts.dotpath, tmp)
|
||||
if os.path.exists(tmp):
|
||||
|
||||
@@ -17,13 +17,14 @@ class Dotfile(DictParser):
|
||||
key_trans_install = 'trans_install'
|
||||
key_trans_update = 'trans_update'
|
||||
key_template = 'template'
|
||||
key_handle_dir_as_block = 'handle_dir_as_block'
|
||||
|
||||
def __init__(self, key, dst, src,
|
||||
actions=None, trans_install=None, trans_update=None,
|
||||
link=LinkTypes.NOLINK, noempty=False,
|
||||
cmpignore=None, upignore=None,
|
||||
instignore=None, template=True, chmod=None,
|
||||
ignore_missing_in_dotdrop=False):
|
||||
ignore_missing_in_dotdrop=False, handle_dir_as_block=False):
|
||||
"""
|
||||
constructor
|
||||
@key: dotfile key
|
||||
@@ -39,6 +40,7 @@ class Dotfile(DictParser):
|
||||
@instignore: patterns to ignore when installing
|
||||
@template: template this dotfile
|
||||
@chmod: file permission
|
||||
@handle_dir_as_block: handle directory as a single block
|
||||
"""
|
||||
self.actions = actions or []
|
||||
self.dst = dst
|
||||
@@ -54,6 +56,7 @@ class Dotfile(DictParser):
|
||||
self.template = template
|
||||
self.chmod = chmod
|
||||
self.ignore_missing_in_dotdrop = ignore_missing_in_dotdrop
|
||||
self.handle_dir_as_block = handle_dir_as_block
|
||||
|
||||
if self.link != LinkTypes.NOLINK and \
|
||||
(
|
||||
@@ -96,6 +99,7 @@ class Dotfile(DictParser):
|
||||
"""patch dict"""
|
||||
value['noempty'] = value.get(cls.key_noempty, False)
|
||||
value['template'] = value.get(cls.key_template, True)
|
||||
value['handle_dir_as_block'] = value.get(cls.key_handle_dir_as_block, False)
|
||||
# remove old entries
|
||||
value.pop(cls.key_noempty, None)
|
||||
return value
|
||||
@@ -121,6 +125,8 @@ class Dotfile(DictParser):
|
||||
msg += f', chmod:{self.chmod:o}'
|
||||
else:
|
||||
msg += f', chmod:\"{self.chmod}\"'
|
||||
if self.handle_dir_as_block:
|
||||
msg += f', handle_dir_as_block:{self.handle_dir_as_block}'
|
||||
return msg
|
||||
|
||||
def prt(self):
|
||||
@@ -136,6 +142,8 @@ class Dotfile(DictParser):
|
||||
out += f'\n{indent}chmod: \"{self.chmod:o}\"'
|
||||
else:
|
||||
out += f'\n{indent}chmod: \"{self.chmod}\"'
|
||||
if self.handle_dir_as_block:
|
||||
out += f'\n{indent}handle_dir_as_block: \"{self.handle_dir_as_block}\"'
|
||||
|
||||
out += f'\n{indent}pre-action:'
|
||||
some = self.get_pre_actions()
|
||||
|
||||
@@ -18,10 +18,11 @@ class FTreeDir:
|
||||
directory tree for comparison
|
||||
"""
|
||||
|
||||
def __init__(self, path, ignores=None, debug=False):
|
||||
def __init__(self, path, ignores=None, debug=False, handle_dir_as_block=False):
|
||||
self.path = path
|
||||
self.ignores = ignores
|
||||
self.debug = debug
|
||||
self.handle_dir_as_block = handle_dir_as_block
|
||||
self.entries = []
|
||||
self.log = Logger(debug=self.debug)
|
||||
if os.path.exists(path) and os.path.isdir(path):
|
||||
@@ -33,6 +34,12 @@ class FTreeDir:
|
||||
ignore empty directory
|
||||
test for ignore pattern
|
||||
"""
|
||||
# if directory should be handled as a block, just add the directory itself
|
||||
if self.handle_dir_as_block:
|
||||
self.log.dbg(f'handle as block: {self.path}')
|
||||
self.entries.append(self.path)
|
||||
return
|
||||
|
||||
for root, dirs, files in os.walk(self.path, followlinks=True):
|
||||
for file in files:
|
||||
fpath = os.path.join(root, file)
|
||||
|
||||
@@ -79,7 +79,7 @@ class Installer:
|
||||
def install(self, templater, src, dst, linktype,
|
||||
actionexec=None, noempty=False,
|
||||
ignore=None, is_template=True,
|
||||
chmod=None):
|
||||
chmod=None, handle_dir_as_block=False):
|
||||
"""
|
||||
install src to dst
|
||||
|
||||
@@ -92,6 +92,7 @@ class Installer:
|
||||
@ignore: pattern to ignore when installing
|
||||
@is_template: this dotfile is a template
|
||||
@chmod: rights to apply if any
|
||||
@handle_dir_as_block: if True, handle directories as a single block
|
||||
|
||||
return
|
||||
- True, None : success
|
||||
@@ -139,7 +140,8 @@ class Installer:
|
||||
actionexec=actionexec,
|
||||
noempty=noempty, ignore=ignore,
|
||||
is_template=is_template,
|
||||
chmod=chmod)
|
||||
chmod=chmod,
|
||||
handle_dir_as_block=handle_dir_as_block)
|
||||
if self.remove_existing_in_dir and ins:
|
||||
self._remove_existing_in_dir(dst, ins)
|
||||
else:
|
||||
@@ -602,7 +604,7 @@ class Installer:
|
||||
def _copy_dir(self, templater, src, dst,
|
||||
actionexec=None, noempty=False,
|
||||
ignore=None, is_template=True,
|
||||
chmod=None):
|
||||
chmod=None, handle_dir_as_block=False):
|
||||
"""
|
||||
install src to dst when is a directory
|
||||
|
||||
@@ -617,6 +619,68 @@ class Installer:
|
||||
fails
|
||||
"""
|
||||
self.log.dbg(f'deploy dir {src}')
|
||||
self.log.dbg(f'handle_dir_as_block: {handle_dir_as_block}')
|
||||
|
||||
# Handle directory as a block if option is enabled
|
||||
if handle_dir_as_block:
|
||||
self.log.dbg(f'handling directory {src} as a block for installation')
|
||||
dst_dotfiles = []
|
||||
|
||||
# Ask user for confirmation if safe mode is on
|
||||
if os.path.exists(dst):
|
||||
msg = f'Overwrite entire directory \"{dst}\" with \"{src}\"?'
|
||||
if self.safe and not self.log.ask(msg):
|
||||
return False, 'aborted', []
|
||||
|
||||
# Remove existing directory completely
|
||||
if self.dry:
|
||||
self.log.dry(f'would rm -r {dst}')
|
||||
else:
|
||||
self.log.dbg(f'rm -r {dst}')
|
||||
if not removepath(dst, logger=self.log):
|
||||
msg = f'unable to remove {dst}, do manually'
|
||||
self.log.warn(msg)
|
||||
return False, msg, []
|
||||
|
||||
# Create parent directory if needed
|
||||
parent_dir = os.path.dirname(dst)
|
||||
if not os.path.exists(parent_dir):
|
||||
if self.dry:
|
||||
self.log.dry(f'would mkdir -p {parent_dir}')
|
||||
else:
|
||||
if not self._create_dirs(parent_dir):
|
||||
err = f'error creating directory for {dst}'
|
||||
return False, err, []
|
||||
|
||||
# Copy directory recursively
|
||||
if self.dry:
|
||||
self.log.dry(f'would cp -r {src} {dst}')
|
||||
return True, None, [dst]
|
||||
else:
|
||||
try:
|
||||
# Execute pre actions
|
||||
ret, err = self._exec_pre_actions(actionexec)
|
||||
if not ret:
|
||||
return False, err, []
|
||||
|
||||
# Copy the directory as a whole
|
||||
shutil.copytree(src, dst)
|
||||
|
||||
# Record all files that were installed
|
||||
for root, _, files in os.walk(dst):
|
||||
for file in files:
|
||||
path = os.path.join(root, file)
|
||||
dst_dotfiles.append(path)
|
||||
|
||||
if not self.comparing:
|
||||
self.log.sub(f'installed directory {src} to {dst} as a block')
|
||||
return True, None, dst_dotfiles
|
||||
except (shutil.Error, OSError) as exc:
|
||||
err = f'{src} installation failed: {exc}'
|
||||
self.log.warn(err)
|
||||
return False, err, []
|
||||
|
||||
# Regular directory installation (file by file)
|
||||
# default to nothing installed and no error
|
||||
ret = False
|
||||
dst_dotfiles = []
|
||||
@@ -644,7 +708,6 @@ class Installer:
|
||||
|
||||
if res:
|
||||
# something got installed
|
||||
|
||||
ret = True
|
||||
else:
|
||||
# is directory
|
||||
|
||||
@@ -39,6 +39,7 @@ class Updater:
|
||||
@debug: enable debug
|
||||
@ignore: pattern to ignore when updating
|
||||
@showpatch: show patch if dotfile to update is a template
|
||||
@ignore_missing_in_dotdrop: ignore missing files in dotdrop
|
||||
"""
|
||||
self.dotpath = dotpath
|
||||
self.variables = variables
|
||||
|
||||
Reference in New Issue
Block a user