1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-16 07:11:10 +00:00

feature for #458

This commit is contained in:
deadc0de6
2025-05-14 15:57:53 +02:00
parent 1bd00b33a7
commit 8dc1af6cd2
6 changed files with 137 additions and 20 deletions

View File

@@ -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)) `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)) `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)) `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)) `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`) `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)) `instignore` | List of patterns to ignore when installing (enclose in quotes when using wildcards; see [ignore patterns](config-file.md#ignore-patterns))
@@ -217,3 +218,24 @@ profiles:
``` ```
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.

View File

@@ -232,12 +232,20 @@ def _dotfile_install(opts, dotfile, tmpdir=None):
LinkTypes.RELATIVE, LinkTypes.ABSOLUTE LinkTypes.RELATIVE, LinkTypes.ABSOLUTE
): ):
# nolink|relative|absolute|link_children # nolink|relative|absolute|link_children
ret, err = inst.install(templ, dotfile.src, dotfile.dst, asblock = False
dotfile.link, if hasattr(dotfile, 'handle_dir_as_block'):
actionexec=pre_actions_exec, asblock = True
is_template=is_template, ret, err = inst.install(
ignore=ignores, templ,
chmod=dotfile.chmod) 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: else:
# nolink # nolink
src = dotfile.src src = dotfile.src
@@ -250,13 +258,21 @@ def _dotfile_install(opts, dotfile, tmpdir=None):
src = tmp src = tmp
# make sure to re-evaluate if is template # make sure to re-evaluate if is template
is_template = dotfile.template and Templategen.path_is_template(src) is_template = dotfile.template and Templategen.path_is_template(src)
ret, err = inst.install(templ, src, dotfile.dst, asblock = False
LinkTypes.NOLINK, if hasattr(dotfile, "handle_dir_as_block"):
actionexec=pre_actions_exec, asblock = True
noempty=dotfile.noempty, ret, err = inst.install(
ignore=ignores, templ,
is_template=is_template, src,
chmod=dotfile.chmod) 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: if tmp:
tmp = os.path.join(opts.dotpath, tmp) tmp = os.path.join(opts.dotpath, tmp)
if os.path.exists(tmp): if os.path.exists(tmp):

View File

@@ -17,13 +17,14 @@ class Dotfile(DictParser):
key_trans_install = 'trans_install' key_trans_install = 'trans_install'
key_trans_update = 'trans_update' key_trans_update = 'trans_update'
key_template = 'template' key_template = 'template'
key_handle_dir_as_block = 'handle_dir_as_block'
def __init__(self, key, dst, src, def __init__(self, key, dst, src,
actions=None, trans_install=None, trans_update=None, actions=None, trans_install=None, trans_update=None,
link=LinkTypes.NOLINK, noempty=False, link=LinkTypes.NOLINK, noempty=False,
cmpignore=None, upignore=None, cmpignore=None, upignore=None,
instignore=None, template=True, chmod=None, instignore=None, template=True, chmod=None,
ignore_missing_in_dotdrop=False): ignore_missing_in_dotdrop=False, handle_dir_as_block=False):
""" """
constructor constructor
@key: dotfile key @key: dotfile key
@@ -39,6 +40,7 @@ class Dotfile(DictParser):
@instignore: patterns to ignore when installing @instignore: patterns to ignore when installing
@template: template this dotfile @template: template this dotfile
@chmod: file permission @chmod: file permission
@handle_dir_as_block: handle directory as a single block
""" """
self.actions = actions or [] self.actions = actions or []
self.dst = dst self.dst = dst
@@ -54,6 +56,7 @@ class Dotfile(DictParser):
self.template = template self.template = template
self.chmod = chmod self.chmod = chmod
self.ignore_missing_in_dotdrop = ignore_missing_in_dotdrop self.ignore_missing_in_dotdrop = ignore_missing_in_dotdrop
self.handle_dir_as_block = handle_dir_as_block
if self.link != LinkTypes.NOLINK and \ if self.link != LinkTypes.NOLINK and \
( (
@@ -96,6 +99,7 @@ class Dotfile(DictParser):
"""patch dict""" """patch dict"""
value['noempty'] = value.get(cls.key_noempty, False) value['noempty'] = value.get(cls.key_noempty, False)
value['template'] = value.get(cls.key_template, True) 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 # remove old entries
value.pop(cls.key_noempty, None) value.pop(cls.key_noempty, None)
return value return value
@@ -121,6 +125,8 @@ class Dotfile(DictParser):
msg += f', chmod:{self.chmod:o}' msg += f', chmod:{self.chmod:o}'
else: else:
msg += f', chmod:\"{self.chmod}\"' msg += f', chmod:\"{self.chmod}\"'
if self.handle_dir_as_block:
msg += f', handle_dir_as_block:{self.handle_dir_as_block}'
return msg return msg
def prt(self): def prt(self):
@@ -136,6 +142,8 @@ class Dotfile(DictParser):
out += f'\n{indent}chmod: \"{self.chmod:o}\"' out += f'\n{indent}chmod: \"{self.chmod:o}\"'
else: else:
out += f'\n{indent}chmod: \"{self.chmod}\"' 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:' out += f'\n{indent}pre-action:'
some = self.get_pre_actions() some = self.get_pre_actions()

View File

@@ -18,10 +18,11 @@ class FTreeDir:
directory tree for comparison 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.path = path
self.ignores = ignores self.ignores = ignores
self.debug = debug self.debug = debug
self.handle_dir_as_block = handle_dir_as_block
self.entries = [] self.entries = []
self.log = Logger(debug=self.debug) self.log = Logger(debug=self.debug)
if os.path.exists(path) and os.path.isdir(path): if os.path.exists(path) and os.path.isdir(path):
@@ -33,6 +34,12 @@ class FTreeDir:
ignore empty directory ignore empty directory
test for ignore pattern 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 root, dirs, files in os.walk(self.path, followlinks=True):
for file in files: for file in files:
fpath = os.path.join(root, file) fpath = os.path.join(root, file)

View File

@@ -79,7 +79,7 @@ class Installer:
def install(self, templater, src, dst, linktype, def install(self, templater, src, dst, linktype,
actionexec=None, noempty=False, actionexec=None, noempty=False,
ignore=None, is_template=True, ignore=None, is_template=True,
chmod=None): chmod=None, handle_dir_as_block=False):
""" """
install src to dst install src to dst
@@ -92,6 +92,7 @@ class Installer:
@ignore: pattern to ignore when installing @ignore: pattern to ignore when installing
@is_template: this dotfile is a template @is_template: this dotfile is a template
@chmod: rights to apply if any @chmod: rights to apply if any
@handle_dir_as_block: if True, handle directories as a single block
return return
- True, None : success - True, None : success
@@ -139,7 +140,8 @@ class Installer:
actionexec=actionexec, actionexec=actionexec,
noempty=noempty, ignore=ignore, noempty=noempty, ignore=ignore,
is_template=is_template, is_template=is_template,
chmod=chmod) chmod=chmod,
handle_dir_as_block=handle_dir_as_block)
if self.remove_existing_in_dir and ins: if self.remove_existing_in_dir and ins:
self._remove_existing_in_dir(dst, ins) self._remove_existing_in_dir(dst, ins)
else: else:
@@ -602,7 +604,7 @@ class Installer:
def _copy_dir(self, templater, src, dst, def _copy_dir(self, templater, src, dst,
actionexec=None, noempty=False, actionexec=None, noempty=False,
ignore=None, is_template=True, ignore=None, is_template=True,
chmod=None): chmod=None, handle_dir_as_block=False):
""" """
install src to dst when is a directory install src to dst when is a directory
@@ -617,6 +619,68 @@ class Installer:
fails fails
""" """
self.log.dbg(f'deploy dir {src}') 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 # default to nothing installed and no error
ret = False ret = False
dst_dotfiles = [] dst_dotfiles = []
@@ -644,7 +708,6 @@ class Installer:
if res: if res:
# something got installed # something got installed
ret = True ret = True
else: else:
# is directory # is directory

View File

@@ -39,6 +39,7 @@ class Updater:
@debug: enable debug @debug: enable debug
@ignore: pattern to ignore when updating @ignore: pattern to ignore when updating
@showpatch: show patch if dotfile to update is a template @showpatch: show patch if dotfile to update is a template
@ignore_missing_in_dotdrop: ignore missing files in dotdrop
""" """
self.dotpath = dotpath self.dotpath = dotpath
self.variables = variables self.variables = variables