Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/config/config-dotfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
`dir_as_block` | List of patterns (globs/regex) to match directories that should be handled as a single block during install operations (see [ignore patterns](config-file.md#ignore-patterns)).
`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))
Expand Down Expand Up @@ -216,4 +217,26 @@ 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 `dir_as_block` option on specific dotfiles:

```yaml
dotfiles:
d_config:
src: app
dst: ~/.config/app
dir_as_block:
- "*app"
- "*otherdir*"
```

Note:
- During **install** operations, any directory matching a pattern in `dir_as_block` will be replaced as a whole
- This option has no effect on **compare** operations, which will always show file-by-file differences
- This option has no effect on dotfiles that are regular files
36 changes: 23 additions & 13 deletions dotdrop/dotdrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,17 @@ 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)
ret, err = inst.install(
templ,
dotfile.src,
dotfile.dst,
dotfile.link,
actionexec=pre_actions_exec,
is_template=is_template,
ignore=ignores,
chmod=dotfile.chmod,
dir_as_block=dotfile.dir_as_block,
)
else:
# nolink
src = dotfile.src
Expand All @@ -250,13 +255,18 @@ 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)
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,
dir_as_block=dotfile.dir_as_block,
)
if tmp:
tmp = os.path.join(opts.dotpath, tmp)
if os.path.exists(tmp):
Expand Down
12 changes: 11 additions & 1 deletion dotdrop/dotfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ class Dotfile(DictParser):
key_trans_install = 'trans_install'
key_trans_update = 'trans_update'
key_template = 'template'
key_dir_as_block = '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, dir_as_block=None):
"""
constructor
@key: dotfile key
Expand All @@ -39,6 +40,7 @@ def __init__(self, key, dst, src,
@instignore: patterns to ignore when installing
@template: template this dotfile
@chmod: file permission
@dir_as_block: handle directory matching patterns as a single block
"""
self.actions = actions or []
self.dst = dst
Expand All @@ -54,6 +56,7 @@ def __init__(self, key, dst, src,
self.template = template
self.chmod = chmod
self.ignore_missing_in_dotdrop = ignore_missing_in_dotdrop
self.dir_as_block = dir_as_block or []

if self.link != LinkTypes.NOLINK and \
(
Expand Down Expand Up @@ -96,6 +99,8 @@ def _adjust_yaml_keys(cls, value):
"""patch dict"""
value['noempty'] = value.get(cls.key_noempty, False)
value['template'] = value.get(cls.key_template, True)
value['dir_as_block'] = value.get(
cls.key_dir_as_block, [])
# remove old entries
value.pop(cls.key_noempty, None)
return value
Expand All @@ -121,6 +126,8 @@ def __str__(self):
msg += f', chmod:{self.chmod:o}'
else:
msg += f', chmod:\"{self.chmod}\"'
if self.dir_as_block:
msg += f', dir_as_block:{self.dir_as_block}'
return msg

def prt(self):
Expand All @@ -136,6 +143,9 @@ def prt(self):
out += f'\n{indent}chmod: \"{self.chmod:o}\"'
else:
out += f'\n{indent}chmod: \"{self.chmod}\"'
if self.dir_as_block:
out += (f'\n{indent}dir_as_block: '
f'"{self.dir_as_block}"')

out += f'\n{indent}pre-action:'
some = self.get_pre_actions()
Expand Down
4 changes: 3 additions & 1 deletion dotdrop/ftree.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class FTreeDir:
directory tree for comparison
"""

def __init__(self, path, ignores=None, debug=False):
def __init__(self, path,
ignores=None,
debug=False):
self.path = path
self.ignores = ignores
self.debug = debug
Expand Down
106 changes: 100 additions & 6 deletions dotdrop/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
handle the installation of dotfiles
"""

# pylint: disable=C0302

import os
import errno
import shutil
import fnmatch

# local imports
from dotdrop.logger import Logger
Expand Down Expand Up @@ -79,7 +82,7 @@ def __init__(self, base='.', create=True, backup=True,
def install(self, templater, src, dst, linktype,
actionexec=None, noempty=False,
ignore=None, is_template=True,
chmod=None):
chmod=None, dir_as_block=None):
"""
install src to dst

Expand All @@ -92,12 +95,14 @@ def install(self, templater, src, dst, linktype,
@ignore: pattern to ignore when installing
@is_template: this dotfile is a template
@chmod: rights to apply if any
@dir_as_block: handle directories matching pattern as a single block

return
- True, None : success
- False, error_msg : error
- False, None : ignored
"""
dir_as_block = dir_as_block or []
if not src or not dst:
# fake dotfile
self.log.dbg('fake dotfile installed')
Expand Down Expand Up @@ -131,6 +136,32 @@ def install(self, templater, src, dst, linktype,
isdir = os.path.isdir(src)
self.log.dbg(f'install {src} to {dst}')
self.log.dbg(f'\"{src}\" is a directory: {isdir}')
self.log.dbg(f'dir_as_block: {dir_as_block}')

treat_as_block = any(
fnmatch.fnmatch(src, pattern)
for pattern in dir_as_block
)
self.log.dbg(
f'dir_as_block patterns: {dir_as_block}, '
f'treat_as_block: {treat_as_block}'
)
if treat_as_block:
self.log.dbg(
f'handling directory {src} '
'as a block for installation'
)
ret, err, ins = self._copy_dir(
templater, src, dst,
actionexec=actionexec,
noempty=noempty, ignore=ignore,
is_template=is_template,
chmod=chmod,
dir_as_block=True
)
if self.remove_existing_in_dir and ins:
self._remove_existing_in_dir(dst, ins)
return self._log_install(ret, err)

if linktype == LinkTypes.NOLINK:
# normal file
Expand All @@ -139,7 +170,8 @@ def install(self, templater, src, dst, linktype,
actionexec=actionexec,
noempty=noempty, ignore=ignore,
is_template=is_template,
chmod=chmod)
chmod=chmod,
dir_as_block=dir_as_block)
if self.remove_existing_in_dir and ins:
self._remove_existing_in_dir(dst, ins)
else:
Expand Down Expand Up @@ -602,7 +634,7 @@ def _copy_file(self, templater, src, dst,
def _copy_dir(self, templater, src, dst,
actionexec=None, noempty=False,
ignore=None, is_template=True,
chmod=None):
chmod=None, dir_as_block=False):
"""
install src to dst when is a directory

Expand All @@ -617,6 +649,69 @@ def _copy_dir(self, templater, src, dst,
fails
"""
self.log.dbg(f'deploy dir {src}')
self.log.dbg(f'dir_as_block: {dir_as_block}')

# Handle directory as a block if option is enabled
if 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]
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 = []
Expand Down Expand Up @@ -644,7 +739,6 @@ def _copy_dir(self, templater, src, dst,

if res:
# something got installed

ret = True
else:
# is directory
Expand All @@ -655,7 +749,8 @@ def _copy_dir(self, templater, src, dst,
actionexec=actionexec,
noempty=noempty,
ignore=ignore,
is_template=is_template)
is_template=is_template,
dir_as_block=dir_as_block)
dst_dotfiles.extend(subs)
if not res and err:
# error occured
Expand Down Expand Up @@ -804,7 +899,6 @@ def _write(self, src, dst, content=None,
########################################################
# helpers
########################################################

@classmethod
def _get_tmp_file_vars(cls, src, dst):
tmp = {}
Expand Down
1 change: 1 addition & 0 deletions dotdrop/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, dotpath, variables, conf,
@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
Expand Down
7 changes: 6 additions & 1 deletion scripts/check-syntax.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ done
# check other python scripts
echo "-----------------------------------------"
echo "checking other python scripts with pylint"
find . -name "*.py" -not -path "./dotdrop/*" -not -regex "\./\.?v?env/.*" | while read -r script; do
find . -name "*.py" \
-not -path "./dotdrop/*" \
-not -path "./build/*" \
-not -path "./dist/*" \
-not -regex "\./\.?v?env/.*" \
| while read -r script; do
echo "checking ${script}"
pylint -sn \
--disable=W0012 \
Expand Down
2 changes: 2 additions & 0 deletions scripts/check_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
VALID_RET = [
200,
302,
429,
]
IGNORES = [
'badgen.net',
'coveralls.io',
'packages.ubuntu.com',
'www.gnu.org',
]
OK_WHEN_FORBIDDEN = [
'linux.die.net',
Expand Down
Loading
Loading