Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add the ability to explicitely specify the dotfile key on import #452

Merged
merged 6 commits into from
Nov 11, 2024
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ for a match with the ignore patterns.

Dotdrop is tested with the use of the [tests.sh](/tests.sh) script.

* Test for PEP8 compliance with `pylint`, `pycodestyle` and `pyflakes` (see [check-syntax.sh](/scripts/test-syntax.sh))
* Test for PEP8 compliance with `pylint`, `pycodestyle` and `pyflakes` (see [check-syntax.sh](/scripts/check-syntax.sh))
* Test the documentation and links (see [check-doc.sh](/scripts/check-doc.sh))
* Run the unittests in [tests directory](/tests) with pytest (see [check-unittest.sh](/scripts/check-unittests.sh))
* Run the blackbox bash script tests in [tests-ng directory](/tests-ng) (see [check-tests-ng.sh](/scripts/check-tests-ng.sh))
Expand Down
2 changes: 1 addition & 1 deletion docs/config/config-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,4 @@ Note:
* directories will **not** be backed up, only files
* when using a different `link` value than `nolink` with directories,
the files under the directory will **not** be backed up
(See [Symlinking dotfiles](config-file.md#symlinking-dotfiles)),
(See [Symlinking dotfiles](config-file.md#symlinking-dotfiles)),
9 changes: 7 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ $ dotdrop import ~/.xinitrc
1 file(s) imported.
```

You can control how the dotfile key is generated in the config file
with the following [config entries](config/config-config.md):
You can explicitely provide the key dotdrop should use for the dotfile entry
in the config file with the `-K --dkey` cli switch. Note that the provided
string will be sanitized for yaml. Also if the key already exists,
it will be appended with `_<incremental_number>` to avoid duplicates.

If the key is not provided, it will be automatically created based on the
following [config entries](config/config-config.md):

* `longkey`
* `false` (default): take the shortest unique path
Expand Down
37 changes: 30 additions & 7 deletions dotdrop/cfg_aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import shlex
import platform
import re
import distro


Expand All @@ -24,6 +25,8 @@


TILD = '~'
YAML_OK = r'[^0-9a-zA-Z.\-_+]+'
YAML_REPL = '_'


class CfgAggregator:
Expand Down Expand Up @@ -69,23 +72,27 @@ def del_dotfile_from_profile(self, dotfile, profile):
return self.cfgyaml.del_dotfile_from_profile(dotfile.key, profile.key)

def new_dotfile(self, src, dst, link, chmod=None,
trans_install=None, trans_update=None):
trans_install=None, trans_update=None,
forcekey=None):
"""
import a new dotfile

@src: path in dotpath
@dst: path in FS
@link: LinkType
@chmod: file permission
@trans_install: read transformation
@trans_update: write transformation
@forcekey: dotfile key
"""
dst = self.path_to_dotfile_dst(dst)
dotfile = self.get_dotfile_by_src_dst(src, dst)
if not dotfile:
# add the dotfile
dotfile = self._create_new_dotfile(src, dst, link, chmod=chmod,
trans_install=trans_install,
trans_update=trans_update)
trans_update=trans_update,
forcekey=forcekey)

if not dotfile:
return False
Expand Down Expand Up @@ -237,10 +244,15 @@ def get_dotfile(self, key, profile_key=None):
########################################################

def _create_new_dotfile(self, src, dst, link, chmod=None,
trans_install=None, trans_update=None):
"""create a new dotfile"""
trans_install=None, trans_update=None,
forcekey=None):
"""
create a new dotfile
"""
# get a new dotfile with a unique key
key = self._get_new_dotfile_key(dst)
key = self._get_new_dotfile_key(dst, forcekey=forcekey)
if not key:
return None
self.log.dbg(f'new dotfile key: {key}')
# add the dotfile
trans_install_key = trans_update_key = None
Expand Down Expand Up @@ -282,6 +294,9 @@ def _load(self, reloading=False):
self.key_prefix = self.settings.key_prefix
self.key_separator = self.settings.key_separator

# clean key separator
self.key_separator = re.sub(YAML_OK, YAML_REPL, self.key_separator)

# dotfiles
self.log.dbg('parsing dotfiles')
self.dotfiles = Dotfile.parse_dict(self.cfgyaml.dotfiles)
Expand Down Expand Up @@ -427,10 +442,17 @@ def _patch_keys_to_objs(self, containers, keys, get_by_key, islist=True):
# dotfile key
########################################################

def _get_new_dotfile_key(self, dst):
def _get_new_dotfile_key(self, dst, forcekey=None):
"""return a new unique dotfile key"""
path = os.path.expanduser(dst)
existing_keys = self.cfgyaml.get_all_dotfile_keys()

# use provided key
if forcekey:
key = self._norm_key_elem(forcekey)
return self._uniq_key(key, existing_keys)

# key creation
path = os.path.expanduser(dst)
if self.settings.longkey:
return self._get_long_key(path, existing_keys)
return self._get_short_key(path, existing_keys)
Expand All @@ -440,6 +462,7 @@ def _norm_key_elem(cls, elem):
"""normalize path element for sanity"""
elem = elem.lstrip('.')
elem = elem.replace(' ', '-')
elem = re.sub(YAML_OK, YAML_REPL, elem)
return elem.lower()

def _get_long_key(self, path, keys):
Expand Down
3 changes: 2 additions & 1 deletion dotdrop/dotdrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,8 @@ def cmd_importer(opts):
dry=opts.dry, safe=opts.safe,
debug=opts.debug,
keepdot=opts.keepdot,
ignore=opts.import_ignore)
ignore=opts.import_ignore,
forcekey=opts.import_force_key)

for path in paths:
tmpret = importer.import_path(path,
Expand Down
10 changes: 7 additions & 3 deletions dotdrop/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ class Importer:

def __init__(self, profile, conf, dotpath, diff_cmd,
variables, dry=False, safe=True, debug=False,
keepdot=True, ignore=None):
keepdot=True, ignore=None, forcekey=None):
"""constructor
@profile: the selected profile
@conf: configuration manager
@conf: configuration manager (CfgAggregator)
@dotpath: dotfiles dotpath
@diff_cmd: diff command to use
@variables: dictionary of variables for the templates
Expand All @@ -37,6 +37,8 @@ def __init__(self, profile, conf, dotpath, diff_cmd,
@debug: enable debug
@keepdot: keep dot prefix
@ignore: patterns to ignore when importing
@forcekey: force the use of a specific dotfile key

This may raise UndefinedException
"""
self.profile = profile
Expand All @@ -51,6 +53,7 @@ def __init__(self, profile, conf, dotpath, diff_cmd,
self.debug = debug
self.keepdot = keepdot
self.ignore = []
self.forcekey = forcekey
self.log = Logger(debug=self.debug)

# patch ignore patterns
Expand Down Expand Up @@ -197,7 +200,8 @@ def _import_in_config(self, path, src, dst, perm,
# add file to config file
retconf = self.conf.new_dotfile(src, dst, linktype, chmod=chmod,
trans_install=trans_install,
trans_update=trans_update)
trans_update=trans_update,
forcekey=self.forcekey)
if not retconf:
self.log.warn(f'\"{path}\" ignored during import')
return 0
Expand Down
5 changes: 4 additions & 1 deletion dotdrop/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
Usage:
dotdrop install [-VbtfndDaWR] [-c <path>] [-p <profile>]
[-w <nb>] [<key>...]
dotdrop import [-Vbdfm] [-c <path>] [-p <profile>] [-i <pattern>...]
dotdrop import [-Vbdfm] [-c <path>] [-p <profile>]
[-i <pattern>...] [--dkey=<key>]
[--transr=<key>] [--transw=<key>]
[-l <link>] [-s <path>] <path>...
dotdrop compare [-LVbz] [-c <path>] [-p <profile>]
Expand All @@ -88,6 +89,7 @@
-G --grepable Grepable output.
-i --ignore=<pattern> Pattern to ignore.
-k --key Treat <path> as a dotfile key.
-K --dkey=<key> Set the dotfile key.
-l --link=<link> Link option (nolink|absolute|relative|link_children).
-L --file-only Do not show diff but only the files that differ.
-m --preserve-mode Insert a chmod entry in the dotfile with its mode.
Expand Down Expand Up @@ -337,6 +339,7 @@ def _apply_args_import(self):
self.import_ignore = uniq_list(self.import_ignore)
self.import_trans_install = self.args['--transr']
self.import_trans_update = self.args['--transw']
self.import_force_key = self.args['--dkey']

def _apply_args_update(self):
"""update specifics"""
Expand Down
135 changes: 135 additions & 0 deletions tests-ng/import-with-key.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# author: deadc0de6 (https://github.com/deadc0de6)
# Copyright (c) 2024, deadc0de6
#
# test with user provided key
# --dkey
#

## start-cookie
set -eu -o errtrace -o pipefail
cur=$(cd "$(dirname "${0}")" && pwd)
ddpath="${cur}/../"
PPATH="{PYTHONPATH:-}"
export PYTHONPATH="${ddpath}:${PPATH}"
altbin="python3 -m dotdrop.dotdrop"
if hash coverage 2>/dev/null; then
mkdir -p coverages/
altbin="coverage run -p --data-file coverages/coverage --source=dotdrop -m dotdrop.dotdrop"
fi
bin="${DT_BIN:-${altbin}}"
# shellcheck source=tests-ng/helpers
source "${cur}"/helpers
echo -e "$(tput setaf 6)==> RUNNING $(basename "${BASH_SOURCE[0]}") <==$(tput sgr0)"
## end-cookie

################################################################
# this is the test
################################################################

# the dotfile source
tmps=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d)
mkdir -p "${tmps}"/dotfiles
# the dotfile destination
tmpd=$(mktemp -d --suffix='-dotdrop-tests' || mktemp -d)
#echo "dotfile destination: ${tmpd}"

clear_on_exit "${tmps}"
clear_on_exit "${tmpd}"

# create the dotfile
echo "file1" > "${tmpd}"/file1

# create the config file
cfg="${tmps}/config.yaml"

cat > "${cfg}" << _EOF
config:
backup: true
create: true
dotpath: dotfiles
dotfiles:
profiles:
_EOF
#cat ${cfg}

####################################################################################
# import
dkey="myfile1"
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/file1
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/file1 ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "${dkey}:" &>/dev/null || ( echo "bad key 1" && exit 1 )

####################################################################################
# import 2 files
echo "firstfile" > "${tmpd}"/firstfile
echo "secondfile" > "${tmpd}"/secondfile

dkey="nfile"
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/firstfile "${tmpd}"/secondfile
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/firstfile ] && echo "not imported in dotpath" && exit 1
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/secondfile ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "${dkey}:" &>/dev/null || ( echo "bad key 2a" && exit 1 )
cat "${cfg}" | grep "${dkey}_1:" &>/dev/null || ( echo "bad key 2b" && exit 1 )

####################################################################################
# import 2 files with bad chars
echo "file-1.1" > "${tmpd}"/file-1.1
echo "file-2.2" > "${tmpd}"/file-2.2

dkey=".bad-key.1 2"
dkey_clean="bad-key.1-2"
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/file-1.1 "${tmpd}"/file-2.2
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/file-1.1 ] && echo "not imported in dotpath" && exit 1
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/file-2.2 ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "${dkey_clean}:" &>/dev/null || ( echo "bad key 3a" && exit 1 )
cat "${cfg}" | grep "${dkey_clean}_1:" &>/dev/null || ( echo "bad key 3b" && exit 1 )

####################################################################################
# re-import
echo "lastfile" > "${tmpd}"/lastfile

dkey="nfile"
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/lastfile
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/lastfile ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "${dkey}_2:" &>/dev/null || ( echo "bad key 4" && exit 1 )

####################################################################################
# bad char
echo "firstfile" > "${tmpd}"/badchar

dkey=".key@#\$ˆ&*()abc0032"
dkey_clean="key_abc0032"
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/badchar
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/badchar ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "${dkey_clean}:" &>/dev/null || ( echo "bad key 5" && exit 1 )

####################################################################################
# empty dkey arg
echo "empty" > "${tmpd}"/empty

dkey=""
cd "${ddpath}" | ${bin} import -f -c "${cfg}" -p p1 -V --dkey "${dkey}" "${tmpd}"/empty
cat "${cfg}"

# test
[ ! -e "${tmps}"/dotfiles/"${tmpd}"/empty ] && echo "not imported in dotpath" && exit 1
cat "${cfg}" | grep "f_empty" &>/dev/null || ( echo "bad key 2a" && exit 1 )

echo "OK"
exit 0
1 change: 1 addition & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def _fake_args():
args['--transw'] = ''
args['--transr'] = ''
args['--remove-existing'] = False
args['--dkey'] = ''
# cmds
args['profiles'] = False
args['files'] = False
Expand Down
Loading