diff --git a/CHANGES b/CHANGES index 96c3c5b..d361d1f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,12 @@ +3.5.0 + * Silence warnings when collecting alt files (#521) + * Adjust handling of encrypt patterns to match 3.3.0 and older + * Make encrypt exclude patterns only match encrypted files + * Automatically exclude alt and template files (#234) + * Support negative alt conditions (#365) + * Handle filenames with space in bash completion (#341) + * Add new yadm.filename template variable (#520) + 3.4.0 * Improve and harden alt file regeneration (#466) * Fix "yadm config" in fish completion (#491) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 7975524..eec1cba 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -9,6 +9,7 @@ Jonathan Daigle Luis López Tin Lai Espen Henriksen +AaronYoung5 Cameron Eagans Klas Mellbourn James Clark diff --git a/Makefile b/Makefile index db48512..cf6695a 100644 --- a/Makefile +++ b/Makefile @@ -123,6 +123,7 @@ testhost: require-docker .testyadm --hostname testhost \ --rm -it \ -v "$(CURDIR)/.testyadm:/bin/yadm:ro" \ + -v "$(CURDIR)/completion/bash/yadm:/usr/share/bash-completion/completions/yadm:ro" \ $(IMAGE) \ bash -l diff --git a/README.md b/README.md index abc7335..dd108e0 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ The star count helps others discover yadm. [master-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=master [master-commits]: https://github.com/yadm-dev/yadm/commits/master [master-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/master.svg?label=master -[obs-badge]: https://img.shields.io/badge/OBS-v3.4.0-blue +[obs-badge]: https://img.shields.io/badge/OBS-v3.5.0-blue [obs-link]: https://software.opensuse.org/download.html?project=home%3ATheLocehiliosan%3Ayadm&package=yadm [releases-badge]: https://img.shields.io/github/tag/yadm-dev/yadm.svg?label=latest+release [releases-link]: https://github.com/yadm-dev/yadm/releases diff --git a/completion/bash/yadm b/completion/bash/yadm index bb3609d..def0c66 100644 --- a/completion/bash/yadm +++ b/completion/bash/yadm @@ -1,88 +1,85 @@ # test if git completion is missing, but loader exists, attempt to load -if ! declare -F _git > /dev/null && ! declare -F __git_wrap__git_main > /dev/null; then - if declare -F _completion_loader > /dev/null; then +if ! declare -F _git >/dev/null && ! declare -F __git_wrap__git_main >/dev/null; then + if declare -F _completion_loader >/dev/null; then _completion_loader git fi fi # only operate if git completion is present -if declare -F _git > /dev/null || declare -F __git_wrap__git_main > /dev/null; then +if declare -F _git >/dev/null || declare -F __git_wrap__git_main >/dev/null; then _yadm() { local current=${COMP_WORDS[COMP_CWORD]} local penultimate - if [ "$((COMP_CWORD-1))" -ge "0" ]; then - penultimate=${COMP_WORDS[COMP_CWORD-1]} + if ((COMP_CWORD >= 1)); then + penultimate=${COMP_WORDS[COMP_CWORD - 1]} fi local antepenultimate - if [ "$((COMP_CWORD-2))" -ge "0" ]; then - antepenultimate=${COMP_WORDS[COMP_CWORD-2]} + if ((COMP_CWORD >= 2)); then + antepenultimate=${COMP_WORDS[COMP_CWORD - 2]} fi local -x GIT_DIR - # shellcheck disable=SC2034 GIT_DIR="$(yadm introspect repo 2>/dev/null)" case "$penultimate" in bootstrap) COMPREPLY=() return 0 - ;; + ;; config) - COMPREPLY=( $(compgen -W "$(yadm introspect configs 2>/dev/null)") ) + COMPREPLY=($(compgen -W "$(yadm introspect configs 2>/dev/null)")) return 0 - ;; + ;; decrypt) - COMPREPLY=( $(compgen -W "-l" -- "$current") ) + COMPREPLY=($(compgen -W "-l" -- "$current")) return 0 - ;; + ;; init) - COMPREPLY=( $(compgen -W "-f -w" -- "$current") ) + COMPREPLY=($(compgen -W "-f -w" -- "$current")) return 0 - ;; + ;; introspect) - COMPREPLY=( $(compgen -W "commands configs repo switches" -- "$current") ) + COMPREPLY=($(compgen -W "commands configs repo switches" -- "$current")) return 0 - ;; + ;; help) COMPREPLY=() # no specific help yet return 0 - ;; + ;; list) - COMPREPLY=( $(compgen -W "-a" -- "$current") ) + COMPREPLY=($(compgen -W "-a" -- "$current")) return 0 - ;; + ;; esac case "$antepenultimate" in clone) - COMPREPLY=( $(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current") ) + COMPREPLY=($(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current")) return 0 - ;; + ;; esac - local yadm_switches=( $(yadm introspect switches 2>/dev/null) ) + local yadm_switches=($(yadm introspect switches 2>/dev/null)) # this condition is so files are completed properly for --yadm-xxx options if [[ " ${yadm_switches[*]} " != *" $penultimate "* ]]; then # TODO: somehow solve the problem with [--yadm-xxx option] being # incompatible with what git expects, namely [--arg=option] - if declare -F _git > /dev/null; then + if declare -F _git >/dev/null; then _git else __git_wrap__git_main fi fi if [[ "$current" =~ ^- ]]; then - local matching - matching=$(compgen -W "${yadm_switches[*]}" -- "$current") - __gitcompappend "$matching" + __gitcompappend "${yadm_switches[*]}" "" "$current" " " fi # Find the index of where the sub-command argument should go. local command_idx - for (( command_idx=1 ; command_idx < ${#COMP_WORDS[@]} ; command_idx++ )); do + for ((command_idx = 1; command_idx < ${#COMP_WORDS[@]}; command_idx++)); do local command_idx_arg="${COMP_WORDS[$command_idx]}" if [[ " ${yadm_switches[*]} " = *" $command_idx_arg "* ]]; then let command_idx++ @@ -93,19 +90,11 @@ if declare -F _git > /dev/null || declare -F __git_wrap__git_main > /dev/null; t fi done if [[ "$COMP_CWORD" = "$command_idx" ]]; then - local matching - matching=$(compgen -W "$(yadm introspect commands 2>/dev/null)" -- "$current") - __gitcompappend "$matching" + __gitcompappend "$(yadm introspect commands 2>/dev/null)" "" "$current" " " fi - - # remove duplicates found in COMPREPLY (a native bash way could be better) - if [ -n "${COMPREPLY[*]}" ]; then - COMPREPLY=($(echo "${COMPREPLY[@]}" | sort -u)) - fi - } - complete -o bashdefault -o default -F _yadm yadm 2>/dev/null \ - || complete -o default -F _yadm yadm + complete -o bashdefault -o default -o nospace -F _yadm yadm 2>/dev/null || + complete -o default -o nospace -F _yadm yadm fi diff --git a/test/conftest.py b/test/conftest.py index 455e5c6..0523355 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -609,14 +609,14 @@ disable-scdaemon env["GNUPGHOME"] = home # this pre-populates std files in the GNUPGHOME - runner(["gpg", "-k"], env=env) + runner(["gpg", "-k"], env=env, report=False) def register_gpg_password(password): """Publish a new GPG mock password and flush cached passwords""" home.join("mock-password").write(password) - runner(["gpgconf", "--reload", "gpg-agent"], env=env) + runner(["gpgconf", "--reload", "gpg-agent"], env=env, report=False) yield data(home, register_gpg_password) - runner(["gpgconf", "--kill", "gpg-agent"], env=env) - runner(["gpgconf", "--remove-socketdir", "gpg-agent"], env=env) + runner(["gpgconf", "--kill", "gpg-agent"], env=env, report=False) + runner(["gpgconf", "--remove-socketdir", "gpg-agent"], env=env, report=False) diff --git a/test/test_alt.py b/test/test_alt.py index ef421f7..138d609 100644 --- a/test/test_alt.py +++ b/test/test_alt.py @@ -217,6 +217,29 @@ def test_auto_alt(runner, yadm_cmd, paths, autoalt): assert str(paths.work.join(source_file)) not in linked +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("autoexclude", [None, "true", "false"]) +def test_alt_exclude(runner, yadm_cmd, paths, autoexclude): + """Test alt exclude""" + + # set the value of auto-exclude + if autoexclude: + os.system(" ".join(yadm_cmd("config", "yadm.auto-exclude", autoexclude))) + + utils.create_alt_files(paths, "##default") + run = runner(yadm_cmd("alt", "-d")) + assert run.success + + run = runner(yadm_cmd("status", "-z", "-uall", "--ignored")) + assert run.success + assert run.err == "" + status = run.out.split("\0") + + for link_path in TEST_PATHS: + flags = "??" if autoexclude == "false" else "!!" + assert f"{flags} {link_path}" in status + + @pytest.mark.usefixtures("ds1_copy") def test_stale_link_removal(runner, yadm_cmd, paths): """Stale links to alternative files are removed diff --git a/test/test_encryption.py b/test/test_encryption.py index 23d8e37..33b7394 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -92,6 +92,7 @@ def encrypt_targets(yadm_cmd, paths): paths.work.join("globs dir/globs file2").write("globs file2") expected.append("globs dir/globs file2") paths.encrypt.write("globs*\n", mode="a") + paths.encrypt.write("globs d*/globs*\n", mode="a") # blank lines paths.encrypt.write("\n \n\t\n", mode="a") @@ -404,8 +405,8 @@ def test_encrypt_added_to_exclude(runner, yadm_cmd, paths, gnupg): run = runner(yadm_cmd("encrypt"), env=env) - assert "test-encrypt-data" in paths.repo.join("info/exclude").read() - assert "original-data" in paths.repo.join("info/exclude").read() + assert "test-encrypt-data" in exclude_file.read() + assert "original-data" in exclude_file.read() assert run.success assert run.err == "" diff --git a/test/test_unit_exclude_encrypted.py b/test/test_unit_exclude_encrypted.py index 99db336..1daa891 100644 --- a/test/test_unit_exclude_encrypted.py +++ b/test/test_unit_exclude_encrypted.py @@ -9,7 +9,12 @@ import pytest def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude): """Test exclude_encrypted()""" - header = "# yadm-auto-excludes\n# This section is managed by yadm.\n# Any edits below will be lost.\n" + header = """\ +# yadm-auto-excludes +# This section is managed by yadm. +# Any edits below will be lost. +# yadm encrypt +""" config_function = 'function config() { echo "false";}' if auto_exclude: @@ -24,7 +29,7 @@ def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, e if exclude == "outdated": exclude_file.write(f"original-exclude\n{header}outdated\n", ensure=True) elif exclude == "up-to-date": - exclude_file.write(f"original-exclude\n{header}test-encrypt-data\n", ensure=True) + exclude_file.write(f"original-exclude\n{header}/test-encrypt-data\n", ensure=True) script = f""" YADM_TEST=1 source {yadm} @@ -42,9 +47,9 @@ def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, e if encrypt_exists: assert exclude_file.exists() if exclude == "missing": - assert exclude_file.read() == f"{header}test-encrypt-data\n" + assert exclude_file.read() == f"{header}/test-encrypt-data\n" else: - assert exclude_file.read() == ("original-exclude\n" f"{header}test-encrypt-data\n") + assert exclude_file.read() == ("original-exclude\n" f"{header}/test-encrypt-data\n") if exclude != "up-to-date": assert f"Updating {exclude_file}" in run.out else: diff --git a/test/test_unit_parse_encrypt.py b/test/test_unit_parse_encrypt.py index 2acac15..935bea4 100644 --- a/test/test_unit_parse_encrypt.py +++ b/test/test_unit_parse_encrypt.py @@ -100,10 +100,11 @@ def create_test_encrypt_data(paths): edata += "*card1\n" # matches same file as the one above paths.work.join("wildcard1").write("", ensure=True) paths.work.join("wildcard2").write("", ensure=True) + paths.work.join("subdir/wildcard1").write("", ensure=True) expected.add("wildcard1") expected.add("wildcard2") - edata += "dirwild*\n" + edata += "dirwild*/file*\n" paths.work.join("dirwildcard/file1").write("", ensure=True) paths.work.join("dirwildcard/file2").write("", ensure=True) expected.add("dirwildcard/file1") @@ -125,6 +126,9 @@ def create_test_encrypt_data(paths): expected.add("ex ex/file4") expected.add("ex ex/file6.text") + paths.work.join("dirwildcard/file7.ex").write("", ensure=True) + expected.add("dirwildcard/file7.ex") + # double star edata += "doublestar/**/file*\n" edata += "!**/file3\n" diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index 9952c0c..d5412c1 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -321,3 +321,76 @@ def test_underscores_and_upper_case_in_distro_and_family(runner, yadm): assert run.success assert run.err == "" assert run.out == expected + + +def test_negative_class_condition(runner, yadm): + """Test negative class condition: returns 0 when matching and proper score when not matching.""" + script = f""" + YADM_TEST=1 source {yadm} + local_class="testclass" + local_classes=("testclass") + + # 0 + score=0 + score_file "filename##~class.testclass" "dest" + echo "score: $score" + + # 16 + score=0 + score_file "filename##~class.badclass" "dest" + echo "score2: $score" + + # 16 + score=0 + score_file "filename##~c.badclass" "dest" + echo "score3: $score" + """ + run = runner(command=["bash"], inp=script) + assert run.success + output = run.out.strip().splitlines() + assert output[0] == "score: 0" + assert output[1] == "score2: 16" + assert output[2] == "score3: 16" + + +def test_negative_combined_conditions(runner, yadm): + """Test negative conditions for multiple alt types: returns 0 when matching and proper score when not matching.""" + script = f""" + YADM_TEST=1 source {yadm} + local_class="testclass" + local_classes=("testclass") + local_distro="testdistro" + + # (0) + (0) = 0 + score=0 + score_file "filename##~class.testclass,~distro.testdistro" "dest" + echo "score: $score" + + # (1000 + 16) + (1000 + 4) = 2020 + score=0 + score_file "filename##class.testclass,distro.testdistro" "dest" + echo "score2: $score" + + # 0 (negated class condition) + score=0 + score_file "filename##~class.badclass,~distro.testdistro" "dest" + echo "score3: $score" + + # (1000 + 16) + (4) = 1020 + score=0 + score_file "filename##class.testclass,~distro.baddistro" "dest" + echo "score4: $score" + + # (1000 + 16) + (16) = 1032 + score=0 + score_file "filename##class.testclass,~class.badclass" "dest" + echo "score5: $score" + """ + run = runner(command=["bash"], inp=script) + assert run.success + output = run.out.strip().splitlines() + assert output[0] == "score: 0" + assert output[1] == "score2: 2020" + assert output[2] == "score3: 0" + assert output[3] == "score4: 1020" + assert output[4] == "score5: 1032" diff --git a/test/test_unit_template_default.py b/test/test_unit_template_default.py index 3c071c5..a4532c2 100644 --- a/test/test_unit_template_default.py +++ b/test/test_unit_template_default.py @@ -141,7 +141,7 @@ end of template INCLUDE_BASIC = "basic\n" INCLUDE_VARIABLES = """\ -included <{{ yadm.class }}> file +included <{{ yadm.class }}> file ({{yadm.filename}}) empty line above """ @@ -151,8 +151,8 @@ TEMPLATE_INCLUDE = """\ The first line {% include empty %} An empty file removes the line above -{%include basic%} -{% include "./variables.{{ yadm.os }}" %} +{%include ./basic%} +{% include "variables.{{ yadm.os }}" %} {% include dir/nested %} Include basic again: {% include basic %} @@ -161,7 +161,7 @@ EXPECTED_INCLUDE = f"""\ The first line An empty file removes the line above basic -included <{LOCAL_CLASS}> file +included <{LOCAL_CLASS}> file (VARIABLES_FILENAME) empty line above no newline at the end @@ -280,6 +280,8 @@ def test_include(runner, yadm, tmpdir): input_file.chmod(FILE_MODE) output_file = tmpdir.join("output") + expected = EXPECTED_INCLUDE.replace("VARIABLES_FILENAME", str(variables_file)) + script = f""" YADM_TEST=1 source {yadm} set_awk @@ -290,7 +292,7 @@ def test_include(runner, yadm, tmpdir): run = runner(command=["bash"], inp=script) assert run.success assert run.err == "" - assert output_file.read() == EXPECTED_INCLUDE + assert output_file.read() == expected assert os.stat(output_file).st_mode == os.stat(input_file).st_mode diff --git a/yadm b/yadm index f0c1403..34d951c 100755 --- a/yadm +++ b/yadm @@ -22,7 +22,7 @@ if [ -z "$BASH_VERSION" ]; then [ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" fi -VERSION=3.4.0 +VERSION=3.5.0 YADM_WORK="$HOME" YADM_DIR= @@ -61,6 +61,7 @@ PROC_VERSION="/proc/version" OPERATING_SYSTEM="Unknown" ENCRYPT_INCLUDE_FILES="unparsed" +NO_ENCRYPT_TRACKED_FILES=() LEGACY_WARNING_ISSUED=0 INVALID_ALT=() @@ -179,39 +180,50 @@ function score_file() { local value=${field#*.} [ "$field" = "$label" ] && value="" # when .value is omitted + # Check for negative condition prefix (e.g., "~