1
0
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:
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))
`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.

View File

@@ -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):

View File

@@ -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()

View File

@@ -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)

View 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

View File

@@ -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