mirror of
https://github.com/deadc0de6/dotdrop.git
synced 2026-02-16 16:45:16 +00:00
feature for #458
This commit is contained in:
22
docs/config/config-dotfiles.md
vendored
22
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))
|
`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.
|
||||||
@@ -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
|
||||||
|
if hasattr(dotfile, 'handle_dir_as_block'):
|
||||||
|
asblock = True
|
||||||
|
ret, err = inst.install(
|
||||||
|
templ,
|
||||||
|
dotfile.src,
|
||||||
|
dotfile.dst,
|
||||||
dotfile.link,
|
dotfile.link,
|
||||||
actionexec=pre_actions_exec,
|
actionexec=pre_actions_exec,
|
||||||
is_template=is_template,
|
is_template=is_template,
|
||||||
ignore=ignores,
|
ignore=ignores,
|
||||||
chmod=dotfile.chmod)
|
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
|
||||||
|
if hasattr(dotfile, "handle_dir_as_block"):
|
||||||
|
asblock = True
|
||||||
|
ret, err = inst.install(
|
||||||
|
templ,
|
||||||
|
src,
|
||||||
|
dotfile.dst,
|
||||||
LinkTypes.NOLINK,
|
LinkTypes.NOLINK,
|
||||||
actionexec=pre_actions_exec,
|
actionexec=pre_actions_exec,
|
||||||
noempty=dotfile.noempty,
|
noempty=dotfile.noempty,
|
||||||
ignore=ignores,
|
ignore=ignores,
|
||||||
is_template=is_template,
|
is_template=is_template,
|
||||||
chmod=dotfile.chmod)
|
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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user