From 95d7bae7b350f209837567ef6c0c5271860683db Mon Sep 17 00:00:00 2001 From: Ross Smith II Date: Wed, 11 Oct 2023 14:10:21 -0700 Subject: [PATCH 01/32] Improve and harden alt file regeneration Improvements include: 1. Skip writing a temporary file if the file contents are unchanged 2. Better error reporting if templating program fails 3. Better error reporting/handling if file creation, mv, or chmod fail 4. Quiet logs by not outputing "Creating output..." line twice (debug & loud) --- yadm | 129 +++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 43 deletions(-) diff --git a/yadm b/yadm index 97a56c1d..12f42d78 100755 --- a/yadm +++ b/yadm @@ -363,7 +363,6 @@ function choose_template_cmd() { function template_default() { input="$1" output="$2" - temp_file="${output}.$$.$RANDOM" # the explicit "space + tab" character class used below is used because not # all versions of awk seem to support the POSIX character classes [[:blank:]] @@ -452,7 +451,10 @@ function conditions() { } EOF - "${AWK_PROGRAM[0]}" \ + local source_dir=$(dirname "$input") + local yadm_classes out + yadm_classes="$(join_string $'\n' "${local_classes[@]}")" + out=$("${AWK_PROGRAM[0]}" \ -v class="$local_class" \ -v arch="$local_arch" \ -v os="$local_system" \ @@ -461,20 +463,20 @@ EOF -v distro="$local_distro" \ -v distro_family="$local_distro_family" \ -v source="$input" \ - -v source_dir="$(dirname "$input")" \ - -v classes="$(join_string $'\n' "${local_classes[@]}")" \ + -v source_dir="$source_dir" \ + -v classes="$yadm_classes" \ "$awk_pgm" \ - "$input" > "$temp_file" || rm -f "$temp_file" + "$input") - move_file "$input" "$output" "$temp_file" + move_file "$input" "$output" "$out" } function template_j2cli() { - input="$1" - output="$2" - temp_file="${output}.$$.$RANDOM" - - YADM_CLASS="$local_class" \ + local input="$1" + local output="$2" + local yadm_classes out + yadm_classes="$(join_string $'\n' "${local_classes[@]}")" + out=$(YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -482,18 +484,18 @@ function template_j2cli() { YADM_DISTRO="$local_distro" \ YADM_DISTRO_FAMILY="$local_distro_family" \ YADM_SOURCE="$input" \ - YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$J2CLI_PROGRAM" "$input" -o "$temp_file" + YADM_CLASSES="$yadm_classes" \ + "$J2CLI_PROGRAM" "$input") - move_file "$input" "$output" "$temp_file" + move_file "$input" "$output" "$out" "$?" } function template_envtpl() { - input="$1" - output="$2" - temp_file="${output}.$$.$RANDOM" - - YADM_CLASS="$local_class" \ + local input="$1" + local output="$2" + local yadm_classes out + yadm_classes="$(join_string $'\n' "${local_classes[@]}")" + out=$(YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -501,19 +503,19 @@ function template_envtpl() { YADM_DISTRO="$local_distro" \ YADM_DISTRO_FAMILY="$local_distro_family" \ YADM_SOURCE="$input" \ - YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$ENVTPL_PROGRAM" --keep-template "$input" -o "$temp_file" + YADM_CLASSES="$yadm_classes" \ + "$ENVTPL_PROGRAM" --keep-template "$input") - move_file "$input" "$output" "$temp_file" + move_file "$input" "$output" "$out" "$?" } function template_esh() { - input="$1" - output="$2" - temp_file="${output}.$$.$RANDOM" + local input="$1" + local output="$2" - YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$ESH_PROGRAM" -o "$temp_file" "$input" \ + local yadm_classes out + yadm_classes="$(join_string $'\n' "${local_classes[@]}")" + out=$("$ESH_PROGRAM" "$input" \ YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ @@ -521,25 +523,61 @@ function template_esh() { YADM_USER="$local_user" \ YADM_DISTRO="$local_distro" \ YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input" + YADM_SOURCE="$input" \ + YADM_CLASSES="$yadm_classes") - move_file "$input" "$output" "$temp_file" + move_file "$input" "$output" "$out" "$?" } function move_file() { - local input=$1 - local output=$2 - local temp_file=$3 + local input="$1" + local output="$2" + local new="$3" + local err="${4:-}" + + if [[ -s "$input" && -z "$new" ]]; then + debug "Failed to create $output from template $input: error $err" + return 1 + fi - [ ! -f "$temp_file" ] && return + if [[ -r "$output" ]]; then + local old + old=$(< "$output") - # if the output files already exists as read-only, change it to be writable. - # there are some environments in which a read-only file will prevent the move - # from being successful. - [[ -e "$output" && ! -w "$output" ]] && chmod u+w "$output" + if [[ "$old" == "$new" ]]; then + debug "Not rewriting file as contents have not changed: $output" + return 0 + fi - mv -f "$temp_file" "$output" - copy_perms "$input" "$output" + # if the output files already exists as read-only, change it to be writable. + # there are some environments in which a read-only file will prevent the move + # from being successful. + + if [[ ! -w "$output" ]]; then + if ! chmod u+w "$output"; then + debug "Unable to make '$output' writeable" + fi + fi + fi + + if [ -n "$loud" ]; then + echo "Creating $output from template $input" + else + debug "Creating $output from template $input" + fi + + local temp_file="${output}.$$.$RANDOM" + if printf '%s' "${new}" >"$temp_file"; then + if mv -f "$temp_file" "$output"; then + copy_perms "$input" "$output" + return + fi + debug "Failed to rename '$temp_file' to '$output'" + rm -f "$temp_file" &>/dev/null + else + debug "Failed to create '$temp_file' to generate '$output'" + fi + return 1 } # ****** yadm Commands ****** @@ -707,8 +745,6 @@ function alt_linking() { template_cmd="${alt_template_cmds[$index]}" if [ -n "$template_cmd" ]; then # a template is defined, process the template - debug "Creating $tgt from template $src" - [ -n "$loud" ] && echo "Creating $tgt from template $src" # ensure the destination path exists assert_parent "$tgt" # remove any existing symlink before processing template @@ -2111,7 +2147,7 @@ function get_mode { # only accept results if they are octal if [[ ! $mode =~ ^[0-7]+$ ]] ; then - mode="" + return fi echo "$mode" @@ -2121,7 +2157,14 @@ function copy_perms { local source="$1" local dest="$2" mode=$(get_mode "$source") - [ -n "$mode" ] && chmod "$mode" "$dest" + if [[ -z "$mode" ]]; then + debug "Unable to get mode for '$source'" + return 1 + fi + if ! chmod "$mode" "$dest"; then + debug "Unable to set mode to '$mode' on '$dest'" + return 1 + fi return 0 } From 16deac67b53fea8e02e88565f344dd2c19bb5b93 Mon Sep 17 00:00:00 2001 From: heddxh Date: Tue, 16 Jul 2024 17:25:44 +0800 Subject: [PATCH 02/32] fix: yadm config in fish completion --- completion/fish/yadm.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completion/fish/yadm.fish b/completion/fish/yadm.fish index ffb9067b..484a098a 100644 --- a/completion/fish/yadm.fish +++ b/completion/fish/yadm.fish @@ -60,7 +60,7 @@ complete -x -c yadm -n '__fish_yadm_using_command introspect' -a (printf -- '%s\ complete -x -c yadm -n '__fish_yadm_needs_command' -a 'gitconfig' -d 'Pass options to the git config command' complete -x -c yadm -n '__fish_yadm_needs_command' -a 'config' -d 'Configure a setting' for name in (yadm introspect configs) - complete -x -c yadm -n '__fish_yadm_using_command config' -a '$name' -d 'yadm config' + complete -x -c yadm -n '__fish_yadm_using_command config' -a $name -d 'yadm config' end # yadm universial options From 12c51e130b2fe042c8945ef5afa724817c9add25 Mon Sep 17 00:00:00 2001 From: "AVM.Martin" Date: Sun, 21 Jul 2024 04:58:42 +0700 Subject: [PATCH 03/32] test(clone): run inside YADM_WORK sub-directory Whenever using `clone` command inside YADM_WORK sub-directory, we need to checkout the correct repo contents. Steps to reproduce: ```bash mkdir $HOME/subdir cd $HOME/subdir yadm clone --bootstrap "" yadm status ``` --- test/test_clone.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_clone.py b/test/test_clone.py index 1cae9297..30beafb4 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -326,3 +326,32 @@ def test_no_repo( def verify_head(paths, branch): """Assert the local repo has the correct head branch""" assert paths.repo.join("HEAD").read() == f"ref: refs/heads/{branch}\n" + + +@pytest.mark.usefixtures("remote") +def test_clone_subdirectory(runner, paths, yadm_cmd, repo_config): + """Test clone from sub-directory of YADM_WORK""" + + # clear out the work path + paths.work.remove() + paths.work.mkdir() + + # create sub-directory + subdir = paths.work.mkdir("subdir") + + # determine remote url + remote_url = f"file://{paths.remote}" + + # run the clone command + args = ["clone", "-w", paths.work] + args += [remote_url] + run = runner(command=yadm_cmd(*args), cwd=subdir) + + # clone should succeed, and repo should be configured properly + assert successful_clone(run, paths, repo_config) + + # test that the conflicts are preserved in the work tree + run = runner(command=yadm_cmd("status", "-uno", "--porcelain"), cwd=subdir) + assert run.success + assert run.out == "" + assert run.err == "" From aba434274e87c96c9e59cb3eaed51d7cf399a7bd Mon Sep 17 00:00:00 2001 From: "AVM.Martin" Date: Sun, 21 Jul 2024 04:58:42 +0700 Subject: [PATCH 04/32] fix(clone): reset index of YADM_WORK --- yadm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yadm b/yadm index 09da278b..fddf6c01 100755 --- a/yadm +++ b/yadm @@ -827,7 +827,7 @@ function clone() { rm -rf "$wc" # then reset the index as the --no-checkout flag makes the index empty - "$GIT_PROGRAM" reset --quiet -- . + "$GIT_PROGRAM" reset --quiet -- "$YADM_WORK" if [ "$YADM_WORK" = "$HOME" ]; then debug "Determining if repo tracks private directories" From 30fa6f08a47ecae701a1b3e5dd0e5eaf5adfe6b3 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 11 Nov 2024 22:30:41 +0100 Subject: [PATCH 05/32] Update testbed docker image * Update base image to Ubuntu 24.10. This uses a python version where j2cli no longer works when installed using pip so use the version from Ubuntu instead which has been patched to work. * Update shellcheck, pylint, pytest, isort, flake8, black and yamllint to the latest versions. This closes #502. * Use a longer expect timeout to fix tests failing when gpg is killed due to this timeout. * Explicitly flush gpg-agent's cached passwords to fix failing tests with latest gnupg. Also clean up after tests to avoid having gpg-agents running after the test (e.g. when running tests directly without docker). --- Makefile | 2 +- pyproject.toml | 1 + test/Dockerfile | 11 +++--- test/conftest.py | 39 +++++++++++++-------- test/pinentry-mock | 5 ++- test/requirements.txt | 12 +++---- test/test_alt.py | 1 + test/test_encryption.py | 2 -- test/test_help.py | 1 + test/test_list.py | 3 +- test/test_unit_choose_template_cmd.py | 7 ++-- test/test_unit_copy_perms.py | 1 + test/test_unit_exclude_encrypted.py | 1 + test/test_unit_issue_legacy_path_warning.py | 1 + test/test_unit_private_dirs.py | 1 + test/test_unit_query_distro.py | 1 + test/test_unit_query_distro_family.py | 1 + test/test_unit_record_score.py | 1 + test/test_unit_relative_path.py | 1 + test/test_unit_remove_stale_links.py | 1 + test/test_unit_report_invalid_alts.py | 1 + test/test_unit_score_file.py | 1 + test/test_unit_set_local_alt_values.py | 1 + test/test_unit_set_yadm_dir.py | 1 + test/test_unit_template_esh.py | 1 + test/test_unit_template_j2.py | 1 + test/test_unit_upgrade.py | 6 ++-- 27 files changed, 66 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 5da09185..a28851fc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTESTS = $(wildcard test/test_*.py) -IMAGE = docker.io/yadm/testbed:2023-07-12 +IMAGE = docker.io/yadm/testbed:2024-11-11 OCI = docker .PHONY: all diff --git a/pyproject.toml b/pyproject.toml index 1e51d44d..e7eecfc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ markers = [ [tool.pylint.design] max-args = 14 +max-positional-arguments = 10 max-locals = 28 max-attributes = 8 max-statements = 65 diff --git a/test/Dockerfile b/test/Dockerfile index 29d863e4..a901d185 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -1,8 +1,7 @@ -FROM ubuntu:23.04 -MAINTAINER Tim Byrne +FROM ubuntu:24.10 # Shellcheck and esh versions -ARG SC_VER=0.9.0 +ARG SC_VER=0.10.0 ARG ESH_VER=0.3.2 # Install prerequisites and configure UTF-8 locale @@ -14,6 +13,7 @@ RUN \ expect \ git \ gnupg \ + j2cli \ locales \ lsb-release \ make \ @@ -39,10 +39,9 @@ RUN cd /opt \ && rm -f shellcheck-v$SC_VER.linux.x86_64.tar.xz \ && ln -s /opt/shellcheck-v$SC_VER/shellcheck /usr/local/bin -# Upgrade pip3 and install requirements +# Install requirements COPY test/requirements.txt /tmp/requirements.txt -RUN python3 -m pip install --break-system-packages --upgrade pip setuptools \ - && python3 -m pip install --break-system-packages --upgrade -r /tmp/requirements.txt \ +RUN python3 -m pip install --break-system-packages -r /tmp/requirements.txt \ && rm -f /tmp/requirements # Install esh diff --git a/test/conftest.py b/test/conftest.py index b18cae43..06993786 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,7 +9,6 @@ import shutil from subprocess import PIPE, Popen -import py import pytest @@ -26,37 +25,37 @@ def pytest_addoption(parser): @pytest.fixture(scope="session") def shellcheck_version(): """Version of shellcheck supported""" - return "0.9.0" + return "0.10.0" @pytest.fixture(scope="session") def pylint_version(): """Version of pylint supported""" - return "2.17.0" + return "3.3.1" @pytest.fixture(scope="session") def isort_version(): """Version of isort supported""" - return "5.12.0" + return "5.13.2" @pytest.fixture(scope="session") def flake8_version(): """Version of flake8 supported""" - return "6.0.0" + return "7.1.1" @pytest.fixture(scope="session") def black_version(): """Version of black supported""" - return "23.1.0" + return "24.10.0" @pytest.fixture(scope="session") def yamllint_version(): """Version of yamllint supported""" - return "1.30.0" + return "1.35.1" @pytest.fixture(scope="session") @@ -246,7 +245,7 @@ def wrap(self, expect): if not expect: return cmdline = " ".join([f'"{w}"' for w in self.command]) - expect_script = f"set timeout 2\nspawn {cmdline}\n" + expect_script = f"set timeout 5\nspawn {cmdline}\n" for question, answer in expect: expect_script += "expect {\n" f'"{question}" {{send "{answer}\\r"}}\n' "timeout {close;exit 128}\n" "}\n" expect_script += "expect eof\n" "foreach {pid spawnid os_error_flag value} [wait] break\n" "exit $value" @@ -575,17 +574,21 @@ def ds1(ds1_work_copy, paths, ds1_dset): def gnupg(tmpdir_factory, runner): """Location of GNUPGHOME""" - def register_gpg_password(password): - """Publish a new GPG mock password""" - py.path.local("/tmp/mock-password").write(password) - home = tmpdir_factory.mktemp("gnupghome") home.chmod(0o700) conf = home.join("gpg.conf") conf.write("no-secmem-warning\n") conf.chmod(0o600) agentconf = home.join("gpg-agent.conf") - agentconf.write(f'pinentry-program {os.path.abspath("test/pinentry-mock")}\n' "max-cache-ttl 0\n") + agentconf.write( + f"""\ +pinentry-program {os.path.abspath("test/pinentry-mock")} +max-cache-ttl 0 +browser-socket none +extra-socket none +disable-scdaemon +""" + ) agentconf.chmod(0o600) data = collections.namedtuple("GNUPG", ["home", "pw"]) env = os.environ.copy() @@ -594,4 +597,12 @@ def register_gpg_password(password): # this pre-populates std files in the GNUPGHOME runner(["gpg", "-k"], env=env) - return data(home, register_gpg_password) + 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) + + yield data(home, register_gpg_password) + + runner(["gpgconf", "--kill", "gpg-agent"], env=env) + runner(["gpgconf", "--remove-socketdir", "gpg-agent"], env=env) diff --git a/test/pinentry-mock b/test/pinentry-mock index 39da0434..288616da 100755 --- a/test/pinentry-mock +++ b/test/pinentry-mock @@ -6,10 +6,9 @@ echo "OK Pleased to meet you" while read -r line; do if [[ $line =~ GETPIN ]]; then - password="$(cat /tmp/mock-password 2>/dev/null)" + password="$(cat "$GNUPGHOME/mock-password" 2>/dev/null)" if [ -n "$password" ]; then - echo -n "D " - echo "$password" + echo "D $password" echo "OK"; else echo "CANCEL"; diff --git a/test/requirements.txt b/test/requirements.txt index e71b349c..1a4c3c17 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,8 +1,8 @@ -black==23.1.0 +black==24.10.0 envtpl -flake8==6.0.0 -isort==5.12.0 +flake8==7.1.1 +isort==5.13.2 j2cli -pylint==2.17.0 -pytest==7.2.2 -yamllint==1.30.0 +pylint==3.3.1 +pytest==8.3.3 +yamllint==1.35.1 diff --git a/test/test_alt.py b/test/test_alt.py index c429ad42..5d6706ef 100644 --- a/test/test_alt.py +++ b/test/test_alt.py @@ -1,4 +1,5 @@ """Test alt""" + import os import string diff --git a/test/test_encryption.py b/test/test_encryption.py index 8c64222a..23d8e370 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -2,7 +2,6 @@ import os import shlex -import time import pytest @@ -219,7 +218,6 @@ def test_symmetric_decrypt(runner, yadm_cmd, paths, decrypt_targets, gnupg, doli if bad_phrase: gnupg.pw("") - time.sleep(1) # allow gpg-agent cache to expire else: gnupg.pw(PASSPHRASE) diff --git a/test/test_help.py b/test/test_help.py index cbfabccf..45203296 100644 --- a/test/test_help.py +++ b/test/test_help.py @@ -1,4 +1,5 @@ """Test help""" + import pytest diff --git a/test/test_list.py b/test/test_list.py index afcea6f7..e9d9e2c5 100644 --- a/test/test_list.py +++ b/test/test_list.py @@ -20,7 +20,8 @@ def test_list(runner, yadm_cmd, paths, ds1, location): run_dir = paths.work elif location == "outside": run_dir = paths.work.join("..") - elif location == "subdir": + else: + assert location == "subdir" # first directory with tracked data run_dir = paths.work.join(ds1.tracked_dirs[0]) with run_dir.as_cwd(): diff --git a/test/test_unit_choose_template_cmd.py b/test/test_unit_choose_template_cmd.py index cf7600b1..9948f807 100644 --- a/test/test_unit_choose_template_cmd.py +++ b/test/test_unit_choose_template_cmd.py @@ -1,4 +1,5 @@ """Unit tests: choose_template_cmd""" + import pytest @@ -19,7 +20,7 @@ def test_kind_default(runner, yadm, awk, label): script = f""" YADM_TEST=1 source {yadm} - function awk_available {{ { awk_avail}; }} + function awk_available {{ {awk_avail}; }} template="$(choose_template_cmd "{label}")" echo "TEMPLATE:$template" """ @@ -50,8 +51,8 @@ def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): script = f""" YADM_TEST=1 source {yadm} - function envtpl_available {{ { envtpl_avail}; }} - function j2cli_available {{ { j2cli_avail}; }} + function envtpl_available {{ {envtpl_avail}; }} + function j2cli_available {{ {j2cli_avail}; }} template="$(choose_template_cmd "{label}")" echo "TEMPLATE:$template" """ diff --git a/test/test_unit_copy_perms.py b/test/test_unit_copy_perms.py index c0ea04fe..bde0d63f 100644 --- a/test/test_unit_copy_perms.py +++ b/test/test_unit_copy_perms.py @@ -1,4 +1,5 @@ """Unit tests: copy_perms""" + import os import pytest diff --git a/test/test_unit_exclude_encrypted.py b/test/test_unit_exclude_encrypted.py index 8937a8b4..99db336b 100644 --- a/test/test_unit_exclude_encrypted.py +++ b/test/test_unit_exclude_encrypted.py @@ -1,4 +1,5 @@ """Unit tests: exclude_encrypted""" + import pytest diff --git a/test/test_unit_issue_legacy_path_warning.py b/test/test_unit_issue_legacy_path_warning.py index faae7faf..602a6958 100644 --- a/test/test_unit_issue_legacy_path_warning.py +++ b/test/test_unit_issue_legacy_path_warning.py @@ -1,4 +1,5 @@ """Unit tests: issue_legacy_path_warning""" + import pytest diff --git a/test/test_unit_private_dirs.py b/test/test_unit_private_dirs.py index 58bd39ca..6812edea 100644 --- a/test/test_unit_private_dirs.py +++ b/test/test_unit_private_dirs.py @@ -1,4 +1,5 @@ """Unit tests: private_dirs""" + import pytest diff --git a/test/test_unit_query_distro.py b/test/test_unit_query_distro.py index c32760b2..18fc5952 100644 --- a/test/test_unit_query_distro.py +++ b/test/test_unit_query_distro.py @@ -1,4 +1,5 @@ """Unit tests: query_distro""" + import pytest diff --git a/test/test_unit_query_distro_family.py b/test/test_unit_query_distro_family.py index 1935bf67..1dca5ee5 100644 --- a/test/test_unit_query_distro_family.py +++ b/test/test_unit_query_distro_family.py @@ -1,4 +1,5 @@ """Unit tests: query_distro_family""" + import pytest diff --git a/test/test_unit_record_score.py b/test/test_unit_record_score.py index a82046cf..3d67b0fb 100644 --- a/test/test_unit_record_score.py +++ b/test/test_unit_record_score.py @@ -1,4 +1,5 @@ """Unit tests: record_score""" + import pytest INIT_VARS = """ diff --git a/test/test_unit_relative_path.py b/test/test_unit_relative_path.py index e0b32f50..cf440a79 100644 --- a/test/test_unit_relative_path.py +++ b/test/test_unit_relative_path.py @@ -1,4 +1,5 @@ """Unit tests: relative_path""" + import pytest diff --git a/test/test_unit_remove_stale_links.py b/test/test_unit_remove_stale_links.py index f389ed80..d92c96b4 100644 --- a/test/test_unit_remove_stale_links.py +++ b/test/test_unit_remove_stale_links.py @@ -1,4 +1,5 @@ """Unit tests: remove_stale_links""" + import os import pytest diff --git a/test/test_unit_report_invalid_alts.py b/test/test_unit_report_invalid_alts.py index 996b1efa..3c7145ff 100644 --- a/test/test_unit_report_invalid_alts.py +++ b/test/test_unit_report_invalid_alts.py @@ -1,4 +1,5 @@ """Unit tests: report_invalid_alts""" + import pytest diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index c84fb1ec..0d3833e7 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -1,4 +1,5 @@ """Unit tests: score_file""" + import pytest CONDITION = { diff --git a/test/test_unit_set_local_alt_values.py b/test/test_unit_set_local_alt_values.py index fa5749d3..d27dedd5 100644 --- a/test/test_unit_set_local_alt_values.py +++ b/test/test_unit_set_local_alt_values.py @@ -1,4 +1,5 @@ """Unit tests: set_local_alt_values""" + import pytest import utils diff --git a/test/test_unit_set_yadm_dir.py b/test/test_unit_set_yadm_dir.py index b56c98d5..058f3f1c 100644 --- a/test/test_unit_set_yadm_dir.py +++ b/test/test_unit_set_yadm_dir.py @@ -1,4 +1,5 @@ """Unit tests: set_yadm_dirs""" + import pytest diff --git a/test/test_unit_template_esh.py b/test/test_unit_template_esh.py index 2c91c206..8583b595 100644 --- a/test/test_unit_template_esh.py +++ b/test/test_unit_template_esh.py @@ -1,4 +1,5 @@ """Unit tests: template_esh""" + import os FILE_MODE = 0o754 diff --git a/test/test_unit_template_j2.py b/test/test_unit_template_j2.py index 750ee8c6..3dbb8a70 100644 --- a/test/test_unit_template_j2.py +++ b/test/test_unit_template_j2.py @@ -1,4 +1,5 @@ """Unit tests: template_j2cli & template_envtpl""" + import os import pytest diff --git a/test/test_unit_upgrade.py b/test/test_unit_upgrade.py index cf4f6f46..5441e226 100644 --- a/test/test_unit_upgrade.py +++ b/test/test_unit_upgrade.py @@ -1,4 +1,5 @@ """Unit tests: upgrade""" + import pytest @@ -62,11 +63,10 @@ def test_upgrade(tmpdir, runner, yadm, condition): function git() {{ echo "$@" if [[ "$*" = *"submodule status" ]]; then - { 'echo " 1234567 mymodule (1.0)"' - if condition == 'submodules' else ':' } + {'echo " 1234567 mymodule (1.0)"' if condition == 'submodules' else ':'} fi if [[ "$*" = *ls-files* ]]; then - return { 1 if condition == 'untracked' else 0 } + return {1 if condition == 'untracked' else 0} fi return 0 }} From a7939bec7ba89c39bd55471ae33103db9d435c6c Mon Sep 17 00:00:00 2001 From: "AVM.Martin" Date: Tue, 26 Nov 2024 19:20:59 +0700 Subject: [PATCH 06/32] style: join arguments --- test/test_clone.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_clone.py b/test/test_clone.py index 30beafb4..266ebcc4 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -343,8 +343,7 @@ def test_clone_subdirectory(runner, paths, yadm_cmd, repo_config): remote_url = f"file://{paths.remote}" # run the clone command - args = ["clone", "-w", paths.work] - args += [remote_url] + args = ["clone", "-w", paths.work, remote_url] run = runner(command=yadm_cmd(*args), cwd=subdir) # clone should succeed, and repo should be configured properly From 85e8c1ddfcbb7d206cc6945fc1fdf25e100c9b4a Mon Sep 17 00:00:00 2001 From: "AVM.Martin" Date: Tue, 26 Nov 2024 19:21:18 +0700 Subject: [PATCH 07/32] docs: fix comment --- test/test_clone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_clone.py b/test/test_clone.py index 266ebcc4..83a96c82 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -349,7 +349,7 @@ def test_clone_subdirectory(runner, paths, yadm_cmd, repo_config): # clone should succeed, and repo should be configured properly assert successful_clone(run, paths, repo_config) - # test that the conflicts are preserved in the work tree + # ensure that no changes found as this is a clean dotfiles clone run = runner(command=yadm_cmd("status", "-uno", "--porcelain"), cwd=subdir) assert run.success assert run.out == "" From ae3a1494497702d16f2331a8d39c136c6e11f066 Mon Sep 17 00:00:00 2001 From: "AVM.Martin" Date: Tue, 26 Nov 2024 19:22:18 +0700 Subject: [PATCH 08/32] style: use pathspec for consistency --- yadm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yadm b/yadm index fddf6c01..2dec60a6 100755 --- a/yadm +++ b/yadm @@ -827,7 +827,7 @@ function clone() { rm -rf "$wc" # then reset the index as the --no-checkout flag makes the index empty - "$GIT_PROGRAM" reset --quiet -- "$YADM_WORK" + "$GIT_PROGRAM" reset --quiet -- ":/" if [ "$YADM_WORK" = "$HOME" ]; then debug "Determining if repo tracks private directories" From 0b91140ea8195ba1718de350c00ad522f09c5fc4 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Fri, 6 Dec 2024 00:07:45 +0100 Subject: [PATCH 09/32] Output the actual paths in help message Fixes #376. --- yadm | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/yadm b/yadm index d347a19d..c3dd0e16 100755 --- a/yadm +++ b/yadm @@ -1174,6 +1174,13 @@ function git_command() { } function help() { + readonly config="${YADM_CONFIG/$HOME/\$HOME}" + readonly encrypt="${YADM_ENCRYPT/$HOME/\$HOME}" + readonly bootstrap="${YADM_BOOTSTRAP/$HOME/\$HOME}" + readonly repo="${YADM_REPO/$HOME/\$HOME}" + readonly archive="${YADM_ARCHIVE/$HOME/\$HOME}" + + readonly padding=" " local msg IFS='' read -r -d '' msg << EOF @@ -1201,11 +1208,11 @@ Commands: yadm transcrypt [OPTIONS] - Run transcrypt commands for the yadm repo Files: - \$HOME/.config/yadm/config - yadm's configuration file - \$HOME/.config/yadm/encrypt - List of globs to encrypt/decrypt - \$HOME/.config/yadm/bootstrap - Script run via: yadm bootstrap - \$HOME/.local/share/yadm/repo.git - yadm's Git repository - \$HOME/.local/share/yadm/archive - Encrypted data stored here + $config${padding:${#config}} - yadm's configuration file + $encrypt${padding:${#encrypt}} - List of globs to encrypt/decrypt + $bootstrap${padding:${#bootstrap}} - Script run via: yadm bootstrap + $repo${padding:${#repo}} - yadm's Git repository + $archive${padding:${#archive}} - Encrypted data stored here Use "man yadm" for complete documentation. EOF From b164d03594b0dd305b2c36b5a3c2d78caf73c899 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Tue, 3 Dec 2024 22:52:58 +0100 Subject: [PATCH 10/32] Make relative_path match full dir and not just a prefix Before this change, relative_path "/A/B/C" "/A/B/CD" would return "" instead of the correct "../CD". --- test/test_unit_relative_path.py | 5 ++ yadm | 99 ++++++++++++++++----------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/test/test_unit_relative_path.py b/test/test_unit_relative_path.py index cf440a79..0d3075fb 100644 --- a/test/test_unit_relative_path.py +++ b/test/test_unit_relative_path.py @@ -11,11 +11,16 @@ ("/A/B/C", "/A/B/C", ""), ("/A/B/C", "/A/B/C/D", "D"), ("/A/B/C", "/A/B/C/D/E", "D/E"), + ("/A/B/C", "/A/B/CD", "../CD"), + ("/A/B/C", "/A/BB/C", "../../BB/C"), ("/A/B/C", "/A/B/D", "../D"), ("/A/B/C", "/A/B/D/E", "../D/E"), ("/A/B/C", "/A/D", "../../D"), ("/A/B/C", "/A/D/E", "../../D/E"), ("/A/B/C", "/D/E/F", "../../../D/E/F"), + ("/", "/A/B/C", "A/B/C"), + ("/A/B/C", "/", "../../.."), + ("/A/B B/C", "/A/C C/D", "../../C C/D"), ], ) def test_relative_path(runner, paths, base, full_path, expected): diff --git a/yadm b/yadm index c3dd0e16..82be8b85 100755 --- a/yadm +++ b/yadm @@ -759,16 +759,13 @@ function alt_linking() { } function ln_relative() { - local full_source full_target target_dir - local full_source="$1" - local full_target="$2" - local target_dir="${full_target%/*}" - if [ "$target_dir" == "" ]; then - target_dir="/" - fi + local source="$1" + local target="$2" + local rel_source - rel_source=$(relative_path "$target_dir" "$full_source") - ln -nfs "$rel_source" "$full_target" + rel_source=$(relative_path "$(builtin_dirname "$target")" "$source") + + ln -nfs "$rel_source" "$target" alt_linked+=("$rel_source") } @@ -2011,61 +2008,59 @@ function parse_encrypt() { function builtin_dirname() { # dirname is not builtin, and universally available, this is a built-in # replacement using parameter expansion - path="$1" - dname="${path%/*}" - if ! [[ "$path" =~ / ]]; then - echo "." - elif [ "$dname" = "" ]; then - echo "/" - else - echo "$dname" + local path="$1" + while [ "${path: -1}" = "/" ]; do + path="${path%/}" + done + + local dir_name="${path%/*}" + while [ "${dir_name: -1}" = "/" ]; do + dir_name="${dir_name%/}" + done + + if [ "$path" = "$dir_name" ]; then + dir_name="." + elif [ -z "$dir_name" ]; then + dir_name="/" fi + echo "$dir_name" } function relative_path() { # Output a path to $2/full, relative to $1/base # - # This fucntion created with ideas from + # This function created with ideas from # https://stackoverflow.com/questions/2564634 - base="$1" - full="$2" - - common_part="$base" - result="" - - count=0 - while [ "${full#"$common_part"}" == "${full}" ]; do - [ "$count" = "500" ] && return # this is a failsafe - # no match, means that candidate common part is not correct - # go up one level (reduce common part) - common_part="$(builtin_dirname "$common_part")" - # and record that we went back, with correct / handling - if [[ -z $result ]]; then - result=".." - else - result="../$result" - fi - count=$((count+1)) - done + local base="$1" + if [ "${base:0:1}" != "/" ]; then + base="$PWD/$base" + fi - if [[ $common_part == "/" ]]; then - # special case for root (no common path) - result="$result/" + local full="$2" + if [ "${full:0:1}" != "/" ]; then + full="$PWD/$full" fi - # since we now have identified the common part, - # compute the non-common part - forward_part="${full#"$common_part"}" + local common_part="$base" + local result="" - # and now stick all parts together - if [[ -n $result ]] && [[ -n $forward_part ]]; then - result="$result$forward_part" - elif [[ -n $forward_part ]]; then - # extra slash removal - result="${forward_part:1}" - fi + while [ "$common_part" != "$full" ]; do + if [ "$common_part" = "/" ]; then + # No common part found. Append / if result is set to make the final + # result correct. + result="${result:+$result/}" + break + elif [ "${full#"$common_part"/}" != "$full" ]; then + common_part="$common_part/" + result="${result:+$result/}" + break + fi + # Move to parent directory and update result + common_part=$(builtin_dirname "$common_part") + result="..${result:+/$result}" + done - echo "$result" + echo "$result${full#"$common_part"}" } # ****** Auto Functions ****** From b2b0b143d685559a73bd19565a66c2c672d28841 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Thu, 28 Nov 2024 23:28:32 +0100 Subject: [PATCH 11/32] Refactor alt handling * Simplify score_file() by using case in instead of nested ifs with regexps. * Merge record_score() and record_template(). * Alt condition processing no longer stops when a template condition is seen but continues processing to verify that all conditions are valid (as the documentation says it should). Fixes #478. * Support alt dirs with deeply nested tracked files (fixes #490). * Use git ls-files to filter out which tracked files to consider for alt processing. Should speed up auto-alt (#505). * Use nocasematch when comparing distro and distro_family. Fixed #455. --- test/test_alt.py | 15 ++ test/test_alt_copy.py | 3 +- test/test_unit_record_score.py | 81 ++++-- test/test_unit_record_template.py | 55 ---- test/test_unit_remove_stale_links.py | 2 +- test/test_unit_score_file.py | 16 +- test/utils.py | 4 +- yadm | 371 ++++++++++++--------------- yadm.1 | 2 +- 9 files changed, 256 insertions(+), 293 deletions(-) delete mode 100644 test/test_unit_record_template.py diff --git a/test/test_alt.py b/test/test_alt.py index 5d6706ef..ef421f77 100644 --- a/test/test_alt.py +++ b/test/test_alt.py @@ -170,6 +170,21 @@ def test_alt_templates(runner, paths, kind, label): assert str(paths.work.join(source_file)) in created +@pytest.mark.usefixtures("ds1_copy") +def test_alt_template_with_condition(runner, paths, tst_arch): + """Test template with extra condition""" + yadm_dir, yadm_data = setup_standard_yadm_dir(paths) + + suffix = f"##template,arch.not{tst_arch}" + utils.create_alt_files(paths, suffix) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) + assert run.success + assert run.err == "" + + created = utils.parse_alt_output(run.out, linked=False) + assert len(created) == 0 + + @pytest.mark.usefixtures("ds1_copy") @pytest.mark.parametrize("autoalt", [None, "true", "false"]) def test_auto_alt(runner, yadm_cmd, paths, autoalt): diff --git a/test/test_alt_copy.py b/test/test_alt_copy.py index e1beece8..de08b8bc 100644 --- a/test/test_alt_copy.py +++ b/test/test_alt_copy.py @@ -40,7 +40,8 @@ def test_alt_copy(runner, yadm_cmd, paths, tst_sys, setting, expect_link, pre_ex run = runner(yadm_cmd("alt")) assert run.success assert run.err == "" - assert "Linking" in run.out + action = "Copying" if setting is True else "Linking" + assert action in run.out assert alt_path.read() == expected_content assert alt_path.islink() == expect_link diff --git a/test/test_unit_record_score.py b/test/test_unit_record_score.py index 3d67b0fb..9049b1be 100644 --- a/test/test_unit_record_score.py +++ b/test/test_unit_record_score.py @@ -19,6 +19,7 @@ echo "SCORES:${alt_scores[@]}" echo "TARGETS:${alt_targets[@]}" echo "SOURCES:${alt_sources[@]}" + echo "TEMPLATE_CMDS:${alt_template_cmds[@]}" """ @@ -38,6 +39,7 @@ def test_dont_record_zeros(runner, yadm): assert "SCORES:\n" in run.out assert "TARGETS:\n" in run.out assert "SOURCES:\n" in run.out + assert "TEMPLATE_CMDS:\n" in run.out def test_new_scores(runner, yadm): @@ -46,9 +48,9 @@ def test_new_scores(runner, yadm): script = f""" YADM_TEST=1 source {yadm} {INIT_VARS} - record_score "1" "tgt_one" "src_one" - record_score "2" "tgt_two" "src_two" - record_score "4" "tgt_three" "src_three" + record_score "1" "tgt_one" "src_one" "" + record_score "2" "tgt_two" "src_two" "" + record_score "4" "tgt_three" "src_three" "" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -58,6 +60,7 @@ def test_new_scores(runner, yadm): assert "SCORES:1 2 4\n" in run.out assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out assert "SOURCES:src_one src_two src_three\n" in run.out + assert "TEMPLATE_CMDS: \n" in run.out @pytest.mark.parametrize("difference", ["lower", "equal", "higher"]) @@ -81,7 +84,8 @@ def test_existing_scores(runner, yadm, difference): alt_scores=(2) alt_targets=("testtgt") alt_sources=("existing_src") - record_score "{score}" "testtgt" "new_src" + alt_template_cmds=("") + record_score "{score}" "testtgt" "new_src" "" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -91,6 +95,7 @@ def test_existing_scores(runner, yadm, difference): assert f"SCORES:{expected_score}\n" in run.out assert "TARGETS:testtgt\n" in run.out assert f"SOURCES:{expected_src}\n" in run.out + assert "TEMPLATE_CMDS:\n" in run.out def test_existing_template(runner, yadm): @@ -101,9 +106,9 @@ def test_existing_template(runner, yadm): {INIT_VARS} alt_scores=(1) alt_targets=("testtgt") - alt_sources=() + alt_sources=("src") alt_template_cmds=("existing_template") - record_score "2" "testtgt" "new_src" + record_score "2" "testtgt" "new_src" "" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -112,7 +117,8 @@ def test_existing_template(runner, yadm): assert "SIZE:1\n" in run.out assert "SCORES:1\n" in run.out assert "TARGETS:testtgt\n" in run.out - assert "SOURCES:\n" in run.out + assert "SOURCES:src\n" in run.out + assert "TEMPLATE_CMDS:existing_template\n" in run.out def test_config_first(runner, yadm): @@ -123,20 +129,61 @@ def test_config_first(runner, yadm): YADM_TEST=1 source {yadm} {INIT_VARS} YADM_CONFIG={config} - record_score "1" "tgt_before" "src_before" - record_template "tgt_tmp" "cmd_tmp" "src_tmp" - record_score "2" "{config}" "src_config" - record_score "3" "tgt_after" "src_after" + record_score "1" "tgt_before" "src_before" "" + record_score "1" "tgt_tmp" "src_tmp" "cmd_tmp" + record_score "2" "{config}" "src_config" "" + record_score "3" "tgt_after" "src_after" "" {REPORT_RESULTS} - echo "CMD_VALUE:${{alt_template_cmds[@]}}" - echo "CMD_INDEX:${{!alt_template_cmds[@]}}" """ run = runner(command=["bash"], inp=script) assert run.success assert run.err == "" - assert "SIZE:3\n" in run.out - assert "SCORES:2 1 3\n" in run.out + assert "SIZE:4\n" in run.out + assert "SCORES:2 1 1 3\n" in run.out assert f"TARGETS:{config} tgt_before tgt_tmp tgt_after\n" in run.out assert "SOURCES:src_config src_before src_tmp src_after\n" in run.out - assert "CMD_VALUE:cmd_tmp\n" in run.out - assert "CMD_INDEX:2\n" in run.out + assert "TEMPLATE_CMDS: cmd_tmp \n" in run.out + + +def test_new_template(runner, yadm): + """Test new template""" + + script = f""" + YADM_TEST=1 source {yadm} + {INIT_VARS} + record_score 0 "tgt_one" "src_one" "cmd_one" + record_score 0 "tgt_two" "src_two" "cmd_two" + record_score 0 "tgt_three" "src_three" "cmd_three" + {REPORT_RESULTS} + """ + run = runner(command=["bash"], inp=script) + assert run.success + assert run.err == "" + assert "SIZE:3\n" in run.out + assert "SCORES:0 0 0\n" in run.out + assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out + assert "SOURCES:src_one src_two src_three\n" in run.out + assert "TEMPLATE_CMDS:cmd_one cmd_two cmd_three\n" in run.out + + +def test_overwrite_existing_template(runner, yadm): + """Overwrite existing templates""" + + script = f""" + YADM_TEST=1 source {yadm} + {INIT_VARS} + alt_scores=(0) + alt_targets=("testtgt") + alt_template_cmds=("existing_cmd") + alt_sources=("existing_src") + record_score 0 "testtgt" "new_src" "new_cmd" + {REPORT_RESULTS} + """ + run = runner(command=["bash"], inp=script) + assert run.success + assert run.err == "" + assert "SIZE:1\n" in run.out + assert "SCORES:0\n" in run.out + assert "TARGETS:testtgt\n" in run.out + assert "SOURCES:new_src\n" in run.out + assert "TEMPLATE_CMDS:new_cmd\n" in run.out diff --git a/test/test_unit_record_template.py b/test/test_unit_record_template.py deleted file mode 100644 index 4f3c3e80..00000000 --- a/test/test_unit_record_template.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Unit tests: record_template""" - -INIT_VARS = """ - alt_targets=() - alt_template_cmds=() - alt_sources=() -""" - -REPORT_RESULTS = """ - echo "SIZE:${#alt_targets[@]}" - echo "TARGETS:${alt_targets[@]}" - echo "CMDS:${alt_template_cmds[@]}" - echo "SOURCES:${alt_sources[@]}" -""" - - -def test_new_template(runner, yadm): - """Test new template""" - - script = f""" - YADM_TEST=1 source {yadm} - {INIT_VARS} - record_template "tgt_one" "cmd_one" "src_one" - record_template "tgt_two" "cmd_two" "src_two" - record_template "tgt_three" "cmd_three" "src_three" - {REPORT_RESULTS} - """ - run = runner(command=["bash"], inp=script) - assert run.success - assert run.err == "" - assert "SIZE:3\n" in run.out - assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out - assert "CMDS:cmd_one cmd_two cmd_three\n" in run.out - assert "SOURCES:src_one src_two src_three\n" in run.out - - -def test_existing_template(runner, yadm): - """Overwrite existing templates""" - - script = f""" - YADM_TEST=1 source {yadm} - {INIT_VARS} - alt_targets=("testtgt") - alt_template_cmds=("existing_cmd") - alt_sources=("existing_src") - record_template "testtgt" "new_cmd" "new_src" - {REPORT_RESULTS} - """ - run = runner(command=["bash"], inp=script) - assert run.success - assert run.err == "" - assert "SIZE:1\n" in run.out - assert "TARGETS:testtgt\n" in run.out - assert "CMDS:new_cmd\n" in run.out - assert "SOURCES:new_src\n" in run.out diff --git a/test/test_unit_remove_stale_links.py b/test/test_unit_remove_stale_links.py index d92c96b4..275832d2 100644 --- a/test/test_unit_remove_stale_links.py +++ b/test/test_unit_remove_stale_links.py @@ -25,7 +25,7 @@ def test_remove_stale_links(runner, yadm, tmpdir, kind, linked): script = f""" YADM_TEST=1 source {yadm} - possible_alts=({link}) + possible_alt_targets=({link}) alt_linked=({alt_linked}) function rm() {{ echo rm "$@"; }} remove_stale_links diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index 0d3833e7..be4631f3 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -89,7 +89,7 @@ def calculate_score(filename): else: score = 0 break - elif label in TEMPLATE_LABELS: + elif label not in TEMPLATE_LABELS: score = 0 break return score @@ -190,7 +190,7 @@ def test_score_values(runner, yadm, default, arch, system, distro, cla, host, us expected = "" for filename, score in filenames.items(): script += f""" - score_file "{filename}" + score_file "{filename}" "dest" echo "{filename}" echo "$score" """ @@ -255,7 +255,7 @@ def test_score_values_templates(runner, yadm): expected = "" for filename, score in filenames.items(): script += f""" - score_file "{filename}" + score_file "{filename}" "dest" echo "{filename}" echo "$score" """ @@ -279,7 +279,7 @@ def test_template_recording(runner, yadm, cmd_generated): script = f""" YADM_TEST=1 source {yadm} - function record_template() {{ echo "template recorded"; }} + function record_score() {{ [ -n "$4" ] && echo "template recorded"; }} {mock} score_file "testfile##template.kind" """ @@ -289,15 +289,15 @@ def test_template_recording(runner, yadm, cmd_generated): assert run.out.rstrip() == expected -def test_underscores_in_distro_and_family(runner, yadm): - """Test replacing spaces in distro / distro_family with underscores""" +def test_underscores_and_upper_case_in_distro_and_family(runner, yadm): + """Test replacing spaces with underscores and lowering case in distro / distro_family""" local_distro = "test distro" local_distro_family = "test family" filenames = { - "filename##distro.test distro": 1004, + "filename##distro.Test Distro": 1004, "filename##distro.test-distro": 0, "filename##distro.test_distro": 1004, - "filename##distro_family.test family": 1008, + "filename##distro_family.test FAMILY": 1008, "filename##distro_family.test-family": 0, "filename##distro_family.test_family": 1008, } diff --git a/test/utils.py b/test/utils.py index c36ecac7..7e6a36d4 100644 --- a/test/utils.py +++ b/test/utils.py @@ -12,7 +12,7 @@ # Directory based alternates must have a tracked contained file. # This will be the test contained file name -CONTAINED = "contained_file" +CONTAINED = "contained_dir/contained_file" # These variables are used for making include files which will be processed # within jinja templates @@ -84,7 +84,7 @@ def parse_alt_output(output, linked=True): """Parse output of 'alt', and return list of linked files""" regex = r"Creating (.+) from template (.+)$" if linked: - regex = r"Linking (.+) to (.+)$" + regex = r"(?:Copy|Link)ing (.+) to (.+)$" parsed_list = {} for line in output.splitlines(): match = re.match(regex, line) diff --git a/yadm b/yadm index 82be8b85..51ed02e2 100755 --- a/yadm +++ b/yadm @@ -166,188 +166,139 @@ function main() { # ****** Alternate Processing ****** function score_file() { - src="$1" - tgt="${src%%##*}" - conditions="${src#*##}" - - if [ "${tgt#"$YADM_ALT/"}" != "${tgt}" ]; then - tgt="${YADM_BASE}/${tgt#"$YADM_ALT/"}" - fi + local source="$1" + local target="$2" + local conditions="${source#*##}" score=0 + local template_cmd="" + IFS=',' read -ra fields <<< "$conditions" for field in "${fields[@]}"; do - label=${field%%.*} - value=${field#*.} + local label=${field%%.*} + local value=${field#*.} [ "$field" = "$label" ] && value="" # when .value is omitted - # extension isn't a condition and doesn't affect the score - if [[ "$label" =~ ^(e|extension)$ ]]; then - continue - fi - score=$((score + 1000)) - # default condition - if [[ "$label" =~ ^(default)$ ]]; then - score=$((score + 0)) - # variable conditions - elif [[ "$label" =~ ^(a|arch)$ ]]; then - if [ "$value" = "$local_arch" ]; then - score=$((score + 1)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(o|os)$ ]]; then - if [ "$value" = "$local_system" ]; then - score=$((score + 2)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(d|distro)$ ]]; then - if [ "${value/\ /_}" = "${local_distro/\ /_}" ]; then - score=$((score + 4)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(f|distro_family)$ ]]; then - if [ "${value/\ /_}" = "${local_distro_family/\ /_}" ]; then - score=$((score + 8)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(c|class)$ ]]; then - if in_list "$value" "${local_classes[@]}"; then - score=$((score + 16)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(h|hostname)$ ]]; then - if [ "$value" = "$local_host" ]; then - score=$((score + 32)) - else - score=0 - return - fi - elif [[ "$label" =~ ^(u|user)$ ]]; then - if [ "$value" = "$local_user" ]; then - score=$((score + 64)) - else - score=0 - return - fi - # templates - elif [[ "$label" =~ ^(t|template|yadm)$ ]]; then - score=0 - cmd=$(choose_template_cmd "$value") - if [ -n "$cmd" ]; then - record_template "$tgt" "$cmd" "$src" - else - debug "No supported template processor for template $src" - [ -n "$loud" ] && echo "No supported template processor for template $src" - fi - return 0 - # unsupported values - else - if [[ "${src##*/}" =~ .\#\#. ]]; then - INVALID_ALT+=("$src") - fi + + local -i delta=-1 + case "$label" in + default) + delta=0 + ;; + a|arch) + [ "$value" = "$local_arch" ] && delta=1 + ;; + o|os) + [ "$value" = "$local_system" ] && delta=2 + ;; + d|distro) + shopt -s nocasematch + [[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 + shopt -u nocasematch + ;; + f|distro_family) + shopt -s nocasematch + [[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 + shopt -u nocasematch + ;; + c|class) + in_list "$value" "${local_classes[@]}" && delta=16 + ;; + h|hostname) + [ "$value" = "$local_host" ] && delta=32 + ;; + u|user) + [ "$value" = "$local_user" ] && delta=64 + ;; + e|extension) + # extension isn't a condition and doesn't affect the score + continue + ;; + t|template|yadm) + if [ -d "$source" ]; then + INVALID_ALT+=("$source") + else + template_cmd=$(choose_template_cmd "$value") + if [ -n "$template_cmd" ]; then + delta=0 + else + debug "No supported template processor for template $source" + [ -n "$loud" ] && echo "No supported template processor for template $source" + fi + fi + ;; + *) + INVALID_ALT+=("$source") + ;; + esac + + if (( delta < 0 )); then score=0 return fi + score=$(( score + 1000 + delta )) done - record_score "$score" "$tgt" "$src" + record_score "$score" "$target" "$source" "$template_cmd" } function record_score() { - score="$1" - tgt="$2" - src="$3" + local score="$1" + local target="$2" + local source="$3" + local template_cmd="$4" # record nothing if the score is zero - [ "$score" -eq 0 ] && return + [ "$score" -eq 0 ] && [ -z "$template_cmd" ] && return # search for the index of this target, to see if we already are tracking it - index=-1 - for search_index in "${!alt_targets[@]}"; do - if [ "${alt_targets[$search_index]}" = "$tgt" ]; then - index="$search_index" - break + local -i index=$((${#alt_targets[@]} - 1)) + for (( ; index >= 0; --index )); do + if [ "${alt_targets[$index]}" = "$target" ]; then + break fi done - # if we don't find an existing index, create one by appending to the array - if [ "$index" -eq -1 ]; then + + if [ $index -lt 0 ]; then # $YADM_CONFIG must be processed first, in case other templates lookup yadm configurations - if [ "$tgt" = "$YADM_CONFIG" ]; then - alt_targets=("$tgt" "${alt_targets[@]}") - alt_sources=("$src" "${alt_sources[@]}") - alt_scores=(0 "${alt_scores[@]}") - index=0 - # increase the index of any existing alt_template_cmds - new_cmds=() - for cmd_index in "${!alt_template_cmds[@]}"; do - new_cmds[cmd_index+1]="${alt_template_cmds[$cmd_index]}" - done - alt_template_cmds=() - for cmd_index in "${!new_cmds[@]}"; do - alt_template_cmds[cmd_index]="${new_cmds[$cmd_index]}" - done - else - alt_targets+=("$tgt") - # set index to the last index (newly created one) - for index in "${!alt_targets[@]}"; do :; done - # and set its initial score to zero - alt_scores[index]=0 - fi - fi + if [ "$target" = "$YADM_CONFIG" ]; then + alt_targets=("$target" "${alt_targets[@]}") - # record nothing if a template command is registered for this file - [ "${alt_template_cmds[$index]+isset}" ] && return + alt_sources=("$source" "${alt_sources[@]}") + alt_scores=("$score" "${alt_scores[@]}") + alt_template_cmds=("$template_cmd" "${alt_template_cmds[@]}") + else + alt_targets+=("$target") - # record higher scoring sources - if [ "$score" -gt "${alt_scores[$index]}" ]; then - alt_scores[index]="$score" - alt_sources[index]="$src" + alt_sources+=("$source") + alt_scores+=("$score") + alt_template_cmds+=("$template_cmd") + fi + return fi -} - -function record_template() { - tgt="$1" - cmd="$2" - src="$3" - - # search for the index of this target, to see if we already are tracking it - index=-1 - for search_index in "${!alt_targets[@]}"; do - if [ "${alt_targets[$search_index]}" = "$tgt" ]; then - index="$search_index" - break + if [[ -n "${alt_template_cmds[$index]}" ]]; then + if [[ -z "$template_cmd" || "$score" -lt "${alt_scores[$index]}" ]]; then + # No template command, or template command but lower score + return fi - done - # if we don't find an existing index, create one by appending to the array - if [ "$index" -eq -1 ]; then - alt_targets+=("$tgt") - # set index to the last index (newly created one) - for index in "${!alt_targets[@]}"; do :; done + elif [[ -z "$template_cmd" && "$score" -le "${alt_scores[$index]}" ]]; then + # No template command and too low score + return fi - # record the template command, last one wins - alt_template_cmds[index]="$cmd" - alt_sources[index]="$src" - + # Record new alt + alt_sources[index]="$source" + alt_scores[index]="$score" + alt_template_cmds[index]="$template_cmd" } function choose_template_cmd() { - kind="$1" + local kind="$1" - if [ "$kind" = "default" ] || [ "$kind" = "" ] && awk_available; then - echo "template_default" - elif [ "$kind" = "esh" ] && esh_available; then - echo "template_esh" + if [ "$kind" = "default" ] || [ "$kind" = "" ]; then + awk_available && echo "template_default" + elif [ "$kind" = "esh" ]; then + esh_available && echo "template_esh" elif [ "$kind" = "j2cli" ] || [ "$kind" = "j2" ] && j2cli_available; then echo "template_j2cli" elif [ "$kind" = "envtpl" ] || [ "$kind" = "j2" ] && envtpl_available; then @@ -488,7 +439,7 @@ EOF -v distro="$local_distro" \ -v distro_family="$local_distro_family" \ -v source="$input" \ - -v source_dir="$(dirname "$input")" \ + -v source_dir="$(builtin_dirname "$input")" \ "$awk_pgm" \ "$input" "${local_classes[@]}" > "$temp_file" || rm -f "$temp_file" @@ -599,29 +550,45 @@ function alt() { # determine all tracked files local tracked_files=() local IFS=$'\n' - for tracked_file in $("$GIT_PROGRAM" ls-files | LC_ALL=C sort); do + for tracked_file in $("$GIT_PROGRAM" ls-files -- '*##*'); do tracked_files+=("$tracked_file") done - # generate data for removing stale links - local possible_alts=() - local IFS=$'\n' - for possible_alt in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do - if [[ $possible_alt =~ .\#\#. ]]; then - base_alt="${possible_alt%%##*}" - yadm_alt="${YADM_BASE}/${base_alt}" - if [ "${yadm_alt#"$YADM_ALT/"}" != "${yadm_alt}" ]; then - base_alt="${yadm_alt#"$YADM_ALT/"}" - fi - possible_alts+=("$YADM_BASE/${base_alt}") + local alt_targets=() + local alt_sources=() + local alt_scores=() + local alt_template_cmds=() + + # For removing stale links + local possible_alt_targets=() + + local alt_source + for alt_source in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do + local conditions="${alt_source#*##}" + if [ "$alt_source" = "$conditions" ]; then + continue + fi + + local target_base="${alt_source%%##*}" + alt_source="${YADM_BASE}/${target_base}##${conditions%%/*}" + local alt_target="${YADM_BASE}/${target_base}" + if [ "${alt_target#"$YADM_ALT/"}" != "$alt_target" ]; then + target_base="${alt_target#"$YADM_ALT/"}" fi + alt_target="${YADM_BASE}/${target_base}" + + if ! in_list "$alt_target" "${possible_alt_targets[@]}"; then + possible_alt_targets+=("$alt_target") + fi + + score_file "$alt_source" "$alt_target" done + local alt_linked=() alt_linking remove_stale_links report_invalid_alts - } function report_invalid_alts() { @@ -662,7 +629,7 @@ function remove_stale_links() { # if a possible alt IS linked, but it's source is not part of alt_linked, # remove it. if readlink_available; then - for stale_candidate in "${possible_alts[@]}"; do + for stale_candidate in "${possible_alt_targets[@]}"; do if [ -L "$stale_candidate" ]; then src=$(readlink "$stale_candidate" 2>/dev/null) if [ -n "$src" ]; then @@ -681,8 +648,8 @@ function set_local_alt_values() { local -a all_classes all_classes=$(config --get-all local.class) while IFS='' read -r class; do - local_classes+=("$class") - local_class="$class" + local_classes+=("$class") + local_class="$class" done <<< "$all_classes" local_arch="$(config local.arch)" @@ -712,50 +679,38 @@ function set_local_alt_values() { } function alt_linking() { + local -i index + for (( index = 0; index < ${#alt_targets[@]}; ++index )); do + local target="${alt_targets[$index]}" + local source="${alt_sources[$index]}" + local template_cmd="${alt_template_cmds[$index]}" + + if [[ -L "$target" ]]; then + rm -f "$target" + elif [[ -d "$target" ]]; then + echo "Skipping alt $source as $target is a directory" + continue + else + assert_parent "$target" + fi - local alt_scores=() - local alt_targets=() - local alt_sources=() - local alt_template_cmds=() + if [[ -n "$template_cmd" ]]; then + debug "Creating $target from template $source" + [[ -n "$loud" ]] && echo "Creating $target from template $source" - for alt_path in $(for tracked in "${tracked_files[@]}"; do printf "%s\n" "$tracked" "${tracked%/*}"; done | LC_ALL=C sort -u) "${ENCRYPT_INCLUDE_FILES[@]}"; do - alt_path="$YADM_BASE/$alt_path" - if [[ "$alt_path" =~ .\#\#. ]]; then - if [ -e "$alt_path" ] ; then - score_file "$alt_path" - fi - fi - done + "$template_cmd" "$source" "$target" + elif [[ "$do_copy" -eq 1 ]]; then + debug "Copying $source to $target" + [[ -n "$loud" ]] && echo "Copying $source to $target" - for index in "${!alt_targets[@]}"; do - tgt="${alt_targets[$index]}" - src="${alt_sources[$index]}" - template_cmd="${alt_template_cmds[$index]}" - if [ -n "$template_cmd" ]; then - # a template is defined, process the template - debug "Creating $tgt from template $src" - [ -n "$loud" ] && echo "Creating $tgt from template $src" - # ensure the destination path exists - assert_parent "$tgt" - # remove any existing symlink before processing template - [ -L "$tgt" ] && rm -f "$tgt" - "$template_cmd" "$src" "$tgt" - elif [ -n "$src" ]; then - # a link source is defined, create symlink - debug "Linking $src to $tgt" - [ -n "$loud" ] && echo "Linking $src to $tgt" - # ensure the destination path exists - assert_parent "$tgt" - if [ "$do_copy" -eq 1 ]; then - # remove any existing symlink before copying - [ -L "$tgt" ] && rm -f "$tgt" - cp -f "$src" "$tgt" - else - ln_relative "$src" "$tgt" - fi + cp -f "$source" "$target" + else + debug "Linking $source to $target" + [[ -n "$loud" ]] && echo "Linking $source to $target" + + ln_relative "$source" "$target" fi done - } function ln_relative() { @@ -765,7 +720,7 @@ function ln_relative() { local rel_source rel_source=$(relative_path "$(builtin_dirname "$target")" "$source") - ln -nfs "$rel_source" "$target" + ln -fs "$rel_source" "$target" alt_linked+=("$rel_source") } diff --git a/yadm.1 b/yadm.1 index 5dd08fd7..b430c4f3 100644 --- a/yadm.1 +++ b/yadm.1 @@ -595,7 +595,7 @@ If no "##default" version exists and no files have valid conditions, then no link will be created. Links are also created for directories named this way, as long as they have at -least one yadm managed file within them (at the top level). +least one yadm managed file within them. yadm will automatically create these links by default. This can be disabled using the From 18d5f66542b9d599997be309d70bd31ece1b549c Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 9 Dec 2024 22:54:41 +0100 Subject: [PATCH 12/32] Ignore case for yadm.distro and .distro_family in default template Same as b2b0b14 but for if statements in default template processor. --- test/test_unit_template_default.py | 4 ++-- yadm | 4 ++++ yadm.1 | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/test_unit_template_default.py b/test/test_unit_template_default.py index aaae0fec..8b0544b5 100644 --- a/test/test_unit_template_default.py +++ b/test/test_unit_template_default.py @@ -86,7 +86,7 @@ {{% if yadm.distro == "wrongdistro1" %}} wrong distro 1 {{% endif %}} -{{% if yadm.distro == "{LOCAL_DISTRO}" %}} +{{% if yadm.distro == "{LOCAL_DISTRO.upper()}" %}} Included section for distro = {{{{yadm.distro}}}} ({{{{yadm.distro}}}} again) {{% endif %}} {{% if yadm.distro == "wrongdistro2" %}} @@ -95,7 +95,7 @@ {{% if yadm.distro_family == "wrongfamily1" %}} wrong family 1 {{% endif %}} -{{% if yadm.distro_family == "{LOCAL_DISTRO_FAMILY}" %}} +{{% if yadm.distro_family == "{LOCAL_DISTRO_FAMILY.upper()}" %}} Included section for distro_family = \ {{{{yadm.distro_family}}}} ({{{{yadm.distro_family}}}} again) {{% endif %}} diff --git a/yadm b/yadm index 51ed02e2..0d7dd982 100755 --- a/yadm +++ b/yadm @@ -363,6 +363,10 @@ BEGIN { if (rhs == cls_array[idx]) { lhs = rhs; break } } } + else if (lhs == "yadm.distro" || lhs == "yadm.distro_family") { + lhs = tolower(replace_vars("{{" lhs "}}")) + rhs = tolower(rhs) + } else { lhs = replace_vars("{{" lhs "}}") } diff --git a/yadm.1 b/yadm.1 index b430c4f3..dcd3bd1b 100644 --- a/yadm.1 +++ b/yadm.1 @@ -502,14 +502,14 @@ See the CONFIGURATION section for more details about setting .BR local.class . .TP .BR distro , " d -Valid if the value matches the distro. +Valid if the value matches the distro (ignoring case). Distro is calculated by running .B "lsb_release -si" or by inspecting the ID from .BR "/etc/os-release" . .TP .BR distro_family , " f -Valid if the value matches the distro family. +Valid if the value matches the distro family (ignoring case). Distro family is calculated by inspecting the ID_LIKE line from .BR "/etc/os-release" . .TP From 6c1970fb417da700157a3430e849a4b8deb087e9 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 9 Dec 2024 23:10:35 +0100 Subject: [PATCH 13/32] Set distro family from ID line if no ID_LIKE line is found See #456. --- test/test_unit_query_distro_family.py | 12 +++++++----- yadm | 10 ++++++---- yadm.1 | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/test/test_unit_query_distro_family.py b/test/test_unit_query_distro_family.py index 1dca5ee5..52fbb315 100644 --- a/test/test_unit_query_distro_family.py +++ b/test/test_unit_query_distro_family.py @@ -3,14 +3,16 @@ import pytest -@pytest.mark.parametrize("condition", ["os-release", "os-release-quotes", "missing"]) +@pytest.mark.parametrize("condition", ["os-release", "os-release-quotes", "missing", "fallback"]) def test_query_distro_family(runner, yadm, tmp_path, condition): """Match ID_LIKE when present""" test_family = "testfamily" os_release = tmp_path.joinpath("os-release") if "os-release" in condition: quotes = '"' if "quotes" in condition else "" - os_release.write_text(f"testing\nID_LIKE={quotes}{test_family}{quotes}\nfamily") + os_release.write_text(f"testing\nID=test\nID_LIKE={quotes}{test_family}{quotes}\nfamily") + elif condition == "fallback": + os_release.write_text(f'testing\nID="{test_family}"\nfamily') script = f""" YADM_TEST=1 source {yadm} OS_RELEASE="{os_release}" @@ -19,7 +21,7 @@ def test_query_distro_family(runner, yadm, tmp_path, condition): run = runner(command=["bash"], inp=script) assert run.success assert run.err == "" - if "os-release" in condition: - assert run.out.rstrip() == test_family - else: + if condition == "missing": assert run.out.rstrip() == "" + else: + assert run.out.rstrip() == test_family diff --git a/yadm b/yadm index 0d7dd982..043934d9 100755 --- a/yadm +++ b/yadm @@ -1499,7 +1499,7 @@ function exclude_encrypted() { } function query_distro() { - distro="" + local distro="" if command -v "$LSB_RELEASE_PROGRAM" &> /dev/null; then distro=$($LSB_RELEASE_PROGRAM -si 2>/dev/null) elif [ -f "$OS_RELEASE" ]; then @@ -1515,17 +1515,19 @@ function query_distro() { } function query_distro_family() { - family="" + local family="" if [ -f "$OS_RELEASE" ]; then while IFS='' read -r line || [ -n "$line" ]; do if [[ "$line" = ID_LIKE=* ]]; then family="${line#ID_LIKE=}" - family="${family//\"}" break + elif [[ "$line" = ID=* ]]; then + family="${line#ID=}" + # No break, only used as fallback in case ID_LIKE isn't found fi done < "$OS_RELEASE" fi - echo "$family" + echo "${family//\"}" } function process_global_args() { diff --git a/yadm.1 b/yadm.1 index dcd3bd1b..6e506d36 100644 --- a/yadm.1 +++ b/yadm.1 @@ -511,7 +511,8 @@ or by inspecting the ID from .BR distro_family , " f Valid if the value matches the distro family (ignoring case). Distro family is calculated by inspecting the ID_LIKE line from -.BR "/etc/os-release" . +.B "/etc/os-release" +(or ID if no ID_LIKE line is found). .TP .BR os , " o Valid if the value matches the OS. From c092b7c099e0fb0d511c27154305c0d4919bee3d Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 9 Dec 2024 23:50:49 +0100 Subject: [PATCH 14/32] Ignore case in alt and default template processor conditions This aligns all conditions with distro and distro_family. Suggestion from #456. --- test/test_unit_score_file.py | 24 ++++++++++----------- test/test_unit_template_default.py | 16 +++++++------- yadm | 34 ++++++++++++------------------ yadm.1 | 10 +++++++-- 4 files changed, 41 insertions(+), 43 deletions(-) diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index be4631f3..af881fd1 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -54,37 +54,37 @@ def calculate_score(filename): if label in CONDITION["default"]["labels"]: score += 1000 elif label in CONDITION["arch"]["labels"]: - if value == "testarch": + if value.lower() == "testarch": score += 1000 + CONDITION["arch"]["modifier"] else: score = 0 break elif label in CONDITION["system"]["labels"]: - if value == "testsystem": + if value.lower() == "testsystem": score += 1000 + CONDITION["system"]["modifier"] else: score = 0 break elif label in CONDITION["distro"]["labels"]: - if value == "testdistro": + if value.lower() == "testdistro": score += 1000 + CONDITION["distro"]["modifier"] else: score = 0 break elif label in CONDITION["class"]["labels"]: - if value == "testclass": + if value.lower() == "testclass": score += 1000 + CONDITION["class"]["modifier"] else: score = 0 break elif label in CONDITION["hostname"]["labels"]: - if value == "testhost": + if value.lower() == "testhost": score += 1000 + CONDITION["hostname"]["modifier"] else: score = 0 break elif label in CONDITION["user"]["labels"]: - if value == "testuser": + if value.lower() == "testuser": score += 1000 + CONDITION["user"]["modifier"] else: score = 0 @@ -105,12 +105,12 @@ def calculate_score(filename): def test_score_values(runner, yadm, default, arch, system, distro, cla, host, user): """Test score results""" # pylint: disable=too-many-branches - local_class = "testclass" - local_arch = "testarch" - local_system = "testsystem" - local_distro = "testdistro" - local_host = "testhost" - local_user = "testuser" + local_class = "testClass" + local_arch = "testARch" + local_system = "TESTsystem" + local_distro = "testDISTro" + local_host = "testHost" + local_user = "testUser" filenames = {"filename##": 0} if default: diff --git a/test/test_unit_template_default.py b/test/test_unit_template_default.py index 8b0544b5..12a493fb 100644 --- a/test/test_unit_template_default.py +++ b/test/test_unit_template_default.py @@ -35,13 +35,13 @@ {{% if yadm.class != "wronglcass" %}} Included section from != {{% endif\t\t %}} -{{% if yadm.class == "{LOCAL_CLASS}" %}} +{{% if yadm.class == "{LOCAL_CLASS.lower()}" %}} Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated) Multiple lines {{% else %}} Should not be included... {{% endif %}} -{{% if yadm.class == "{LOCAL_CLASS2}" %}} +{{% if yadm.class == "{LOCAL_CLASS2.upper()}" %}} Included section for second class {{% endif %}} {{% if yadm.class == "wrongclass2" %}} @@ -50,7 +50,7 @@ {{% if yadm.arch == "wrongarch1" %}} wrong arch 1 {{% endif %}} -{{% if yadm.arch == "{LOCAL_ARCH}" %}} +{{% if yadm.arch == "{LOCAL_ARCH.title()}" %}} Included section for arch = {{{{yadm.arch}}}} ({{{{yadm.arch}}}} repeated) {{% endif %}} {{% if yadm.arch == "wrongarch2" %}} @@ -59,7 +59,7 @@ {{% if yadm.os == "wrongos1" %}} wrong os 1 {{% endif %}} -{{% if yadm.os == "{LOCAL_SYSTEM}" %}} +{{% if yadm.os == "{LOCAL_SYSTEM.lower()}" %}} Included section for os = {{{{yadm.os}}}} ({{{{yadm.os}}}} repeated) {{% endif %}} {{% if yadm.os == "wrongos2" %}} @@ -68,7 +68,7 @@ {{% if yadm.hostname == "wronghost1" %}} wrong host 1 {{% endif %}} -{{% if yadm.hostname == "{LOCAL_HOST}" %}} +{{% if yadm.hostname == "{LOCAL_HOST.upper()}" %}} Included section for host = {{{{yadm.hostname}}}} ({{{{yadm.hostname}}}} again) {{% endif %}} {{% if yadm.hostname == "wronghost2" %}} @@ -77,7 +77,7 @@ {{% if yadm.user == "wronguser1" %}} wrong user 1 {{% endif %}} -{{% if yadm.user == "{LOCAL_USER}" %}} +{{% if yadm.user == "{LOCAL_USER.title()}" %}} Included section for user = {{{{yadm.user}}}} ({{{{yadm.user}}}} repeated) {{% endif %}} {{% if yadm.user == "wronguser2" %}} @@ -86,7 +86,7 @@ {{% if yadm.distro == "wrongdistro1" %}} wrong distro 1 {{% endif %}} -{{% if yadm.distro == "{LOCAL_DISTRO.upper()}" %}} +{{% if yadm.distro == "{LOCAL_DISTRO.lower()}" %}} Included section for distro = {{{{yadm.distro}}}} ({{{{yadm.distro}}}} again) {{% endif %}} {{% if yadm.distro == "wrongdistro2" %}} @@ -102,7 +102,7 @@ {{% if yadm.distro_family == "wrongfamily2" %}} wrong family 2 {{% endif %}} -{{% if env.VAR == "{ENV_VAR}" %}} +{{% if env.VAR == "{ENV_VAR.title()}" %}} Included section for env.VAR = {{{{env.VAR}}}} ({{{{env.VAR}}}} again) {{% endif %}} {{% if env.VAR == "wrongenvvar" %}} diff --git a/yadm b/yadm index 043934d9..0c120465 100755 --- a/yadm +++ b/yadm @@ -179,35 +179,32 @@ function score_file() { local value=${field#*.} [ "$field" = "$label" ] && value="" # when .value is omitted + shopt -s nocasematch local -i delta=-1 case "$label" in default) delta=0 ;; a|arch) - [ "$value" = "$local_arch" ] && delta=1 + [[ "$value" = "$local_arch" ]] && delta=1 ;; o|os) - [ "$value" = "$local_system" ] && delta=2 + [[ "$value" = "$local_system" ]] && delta=2 ;; d|distro) - shopt -s nocasematch [[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 - shopt -u nocasematch ;; f|distro_family) - shopt -s nocasematch [[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 - shopt -u nocasematch ;; c|class) in_list "$value" "${local_classes[@]}" && delta=16 ;; h|hostname) - [ "$value" = "$local_host" ] && delta=32 + [[ "$value" = "$local_host" ]] && delta=32 ;; u|user) - [ "$value" = "$local_user" ] && delta=64 + [[ "$value" = "$local_user" ]] && delta=64 ;; e|extension) # extension isn't a condition and doesn't affect the score @@ -230,6 +227,7 @@ function score_file() { INVALID_ALT+=("$source") ;; esac + shopt -u nocasematch if (( delta < 0 )); then score=0 @@ -295,16 +293,14 @@ function record_score() { function choose_template_cmd() { local kind="$1" - if [ "$kind" = "default" ] || [ "$kind" = "" ]; then + if [[ "${kind:-default}" = "default" ]]; then awk_available && echo "template_default" - elif [ "$kind" = "esh" ]; then + elif [[ "$kind" = "esh" ]]; then esh_available && echo "template_esh" - elif [ "$kind" = "j2cli" ] || [ "$kind" = "j2" ] && j2cli_available; then + elif [[ "$kind" = "j2cli" || "$kind" = "j2" ]] && j2cli_available; then echo "template_j2cli" - elif [ "$kind" = "envtpl" ] || [ "$kind" = "j2" ] && envtpl_available; then + elif [[ "$kind" = "envtpl" || "$kind" = "j2" ]] && envtpl_available; then echo "template_envtpl" - else - return # this "kind" of template is not supported fi } @@ -354,21 +350,17 @@ BEGIN { match($0, /[!=]=/) op = substr($0, RSTART, RLENGTH) match($0, /".*"/) - rhs = replace_vars(substr($0, RSTART + 1, RLENGTH - 2)) + rhs = tolower(replace_vars(substr($0, RSTART + 1, RLENGTH - 2))) if (lhs == "yadm.class") { lhs = "not" rhs split(classes, cls_array, "\n") for (idx in cls_array) { - if (rhs == cls_array[idx]) { lhs = rhs; break } + if (rhs == tolower(cls_array[idx])) { lhs = rhs; break } } } - else if (lhs == "yadm.distro" || lhs == "yadm.distro_family") { - lhs = tolower(replace_vars("{{" lhs "}}")) - rhs = tolower(rhs) - } else { - lhs = replace_vars("{{" lhs "}}") + lhs = tolower(replace_vars("{{" lhs "}}")) } if (op == "==") { skip[++level] = lhs != rhs } diff --git a/yadm.1 b/yadm.1 index 6e506d36..121e9109 100644 --- a/yadm.1 +++ b/yadm.1 @@ -474,6 +474,9 @@ be omitted. Most attributes can be abbreviated as a single letter. [.] +.BR NOTE : +Value is compared case-insensitive. + These are the supported attributes, in the order of the weighted precedence: .TP @@ -502,14 +505,14 @@ See the CONFIGURATION section for more details about setting .BR local.class . .TP .BR distro , " d -Valid if the value matches the distro (ignoring case). +Valid if the value matches the distro. Distro is calculated by running .B "lsb_release -si" or by inspecting the ID from .BR "/etc/os-release" . .TP .BR distro_family , " f -Valid if the value matches the distro family (ignoring case). +Valid if the value matches the distro family. Distro family is calculated by inspecting the ID_LIKE line from .B "/etc/os-release" (or ID if no ID_LIKE line is found). @@ -638,6 +641,9 @@ upon .BR awk , which is available on most *nix systems. To use this processor, specify the value of "default" or just leave the value off (e.g. "##template"). + +.BR NOTE : +This template processor performs case-insensitive comparisions in if statements. .TP .B ESH ESH is a template processor written in POSIX compliant shell. It allows From 8c2f833b4375198341d0a43698fdf83e5ddcec16 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Tue, 10 Dec 2024 18:35:26 +0100 Subject: [PATCH 15/32] Support overriding distro and distro family Fixes #430. --- test/conftest.py | 2 + test/test_unit_set_local_alt_values.py | 70 +++++++++----------------- yadm | 23 ++++++--- yadm.1 | 16 ++++-- 4 files changed, 55 insertions(+), 56 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 06993786..7a576901 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -139,6 +139,8 @@ def supported_configs(): return [ "local.arch", "local.class", + "local.distro", + "local.distro-family", "local.hostname", "local.os", "local.user", diff --git a/test/test_unit_set_local_alt_values.py b/test/test_unit_set_local_alt_values.py index d27dedd5..f7d3877f 100644 --- a/test/test_unit_set_local_alt_values.py +++ b/test/test_unit_set_local_alt_values.py @@ -13,6 +13,8 @@ "os", "hostname", "user", + "distro", + "distro-family", ], ids=[ "no-override", @@ -21,11 +23,15 @@ "override-os", "override-hostname", "override-user", + "override-distro", + "override-distro-family", ], ) @pytest.mark.usefixtures("ds1_copy") -def test_set_local_alt_values(runner, yadm, paths, tst_arch, tst_sys, tst_host, tst_user, override): - """Use issue_legacy_path_warning""" +def test_set_local_alt_values( + runner, yadm, paths, tst_arch, tst_sys, tst_host, tst_user, tst_distro, tst_distro_family, override +): + """Test handling of local alt values""" script = f""" YADM_TEST=1 source {yadm} && set_operating_system && @@ -34,8 +40,10 @@ def test_set_local_alt_values(runner, yadm, paths, tst_arch, tst_sys, tst_host, echo "class='$local_class'" echo "arch='$local_arch'" echo "os='$local_system'" - echo "host='$local_host'" + echo "hostname='$local_host'" echo "user='$local_user'" + echo "distro='$local_distro'" + echo "distro-family='$local_distro_family'" """ if override == "class": @@ -48,46 +56,18 @@ def test_set_local_alt_values(runner, yadm, paths, tst_arch, tst_sys, tst_host, assert run.success assert run.err == "" - if override == "class": - assert "class='override'" in run.out - else: - assert "class=''" in run.out - - if override == "arch": - assert "arch='override'" in run.out - else: - assert f"arch='{tst_arch}'" in run.out - - if override == "os": - assert "os='override'" in run.out - else: - assert f"os='{tst_sys}'" in run.out - - if override == "hostname": - assert "host='override'" in run.out - else: - assert f"host='{tst_host}'" in run.out - - if override == "user": - assert "user='override'" in run.out - else: - assert f"user='{tst_user}'" in run.out - - -def test_distro_and_family(runner, yadm): - """Assert that local_distro/local_distro_family are set""" + default_values = { + "class": "", + "arch": tst_arch, + "os": tst_sys, + "hostname": tst_host, + "user": tst_user, + "distro": tst_distro, + "distro-family": tst_distro_family, + } - script = f""" - YADM_TEST=1 source {yadm} - function config() {{ echo "$1"; }} - function query_distro() {{ echo "testdistro"; }} - function query_distro_family() {{ echo "testfamily"; }} - set_local_alt_values - echo "distro='$local_distro'" - echo "distro_family='$local_distro_family'" - """ - run = runner(command=["bash"], inp=script) - assert run.success - assert run.err == "" - assert "distro='testdistro'" in run.out - assert "distro_family='testfamily'" in run.out + for key, value in default_values.items(): + if key == override: + assert f"{key}='override'" in run.out + else: + assert f"{key}='{value}'" in run.out diff --git a/yadm b/yadm index 0c120465..7b68f546 100755 --- a/yadm +++ b/yadm @@ -649,28 +649,35 @@ function set_local_alt_values() { done <<< "$all_classes" local_arch="$(config local.arch)" - if [ -z "$local_arch" ] ; then + if [[ -z "$local_arch" ]]; then local_arch=$(uname -m) fi local_system="$(config local.os)" - if [ -z "$local_system" ] ; then + if [[ -z "$local_system" ]]; then local_system="$OPERATING_SYSTEM" fi local_host="$(config local.hostname)" - if [ -z "$local_host" ] ; then + if [[ -z "$local_host" ]]; then local_host=$(uname -n) local_host=${local_host%%.*} # trim any domain from hostname fi local_user="$(config local.user)" - if [ -z "$local_user" ] ; then + if [[ -z "$local_user" ]]; then local_user=$(id -u -n) fi - local_distro="$(query_distro)" - local_distro_family="$(query_distro_family)" + local_distro="$(config local.distro)" + if [[ -z "$local_distro" ]]; then + local_distro="$(query_distro)" + fi + + local_distro_family="$(config local.distro-family)" + if [[ -z "$local_distro_family" ]]; then + local_distro_family="$(query_distro_family)" + fi } @@ -849,7 +856,7 @@ EOF function config() { use_repo_config=0 - local_options="^local\.(class|arch|os|hostname|user)$" + local_options="^local\.(class|arch|os|hostname|user|distro|distro-family)$" for option in "$@"; do [[ "$option" =~ $local_options ]] && use_repo_config=1 done @@ -1230,6 +1237,8 @@ function introspect_configs() { read -r -d '' msg <<-EOF local.arch local.class +local.distro +local.distro-family local.hostname local.os local.user diff --git a/yadm.1 b/yadm.1 index 121e9109..1f36498a 100644 --- a/yadm.1 +++ b/yadm.1 @@ -427,7 +427,7 @@ Disable the permission changes to This feature is enabled by default. .RE -The following five "local" configurations are not stored in the +The following "local" configurations are not stored in the .IR $HOME/.config/yadm/config, they are stored in the local repository. @@ -452,6 +452,12 @@ Override the OS for the purpose of symlinking alternate files. .TP .B local.user Override the user for the purpose of symlinking alternate files. +.TP +.B local.distro +Override the distro for the purpose of symlinking alternate files. +.TP +.B local.distro-family +Override the distro family for the purpose of symlinking alternate files. .SH ALTERNATES @@ -618,13 +624,15 @@ command. The following sets the class to be "Work". yadm config local.class Work -Similarly, the values of architecture, os, hostname, and user can be manually -overridden using the configuration options +Similarly, the values of architecture, os, hostname, user, distro, and +distro_family can be manually overridden using the configuration options .BR local.arch , .BR local.os , .BR local.hostname , +.BR local.user , +.BR local.distro , and -.BR local.user . +.BR local.distro-family . .SH TEMPLATES From c23289479245535a1f50900dfeed6b9cfe1ec12f Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 20:34:51 +0100 Subject: [PATCH 16/32] Make test_unit_configure_paths work with bash 3 Bash 3 doesn't understand declare -p so use regular parameter expansion instead to print all variables that begin with YADM_ or GIT_. --- test/test_unit_configure_paths.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_unit_configure_paths.py b/test/test_unit_configure_paths.py index d2a680e8..9d7b1a5e 100644 --- a/test/test_unit_configure_paths.py +++ b/test/test_unit_configure_paths.py @@ -89,7 +89,9 @@ def run_test(runner, paths, args, expected_matches, cwd=None): XDG_DATA_HOME= HOME="{HOME}" set_yadm_dirs configure_paths - declare -p | grep -E '(YADM|GIT)_' + for var in "${{!YADM_@}}" "${{!GIT_@}}"; do + echo "$var=\\"${{!var}}\\"" + done """ run = runner(command=["bash"], inp=script, cwd=cwd) assert run.success From 3093460d41a7203f751f990e46b9553981eb6e8c Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 20:38:15 +0100 Subject: [PATCH 17/32] Ignore __pycache__/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 53249476..7f43fdb6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .testyadm _site testenv +__pycache__/ From 24772e7b4b9fd9012ac72beba473837494f8f4cc Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 20:40:06 +0100 Subject: [PATCH 18/32] Fix test_upgrade when running outside of docker image E.g. direct on github runner. --- test/test_upgrade.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_upgrade.py b/test/test_upgrade.py index 8ab1e949..060c77ed 100644 --- a/test/test_upgrade.py +++ b/test/test_upgrade.py @@ -19,13 +19,15 @@ ], ) @pytest.mark.parametrize("submodule", [False, True], ids=["no submodule", "with submodules"]) -def test_upgrade(tmpdir, runner, versions, submodule): +def test_upgrade(tmpdir, runner, paths, versions, submodule): """Upgrade tests""" # pylint: disable=too-many-statements home = tmpdir.mkdir("HOME") env = {"HOME": str(home)} runner(["git", "config", "--global", "init.defaultBranch", "master"], env=env) runner(["git", "config", "--global", "protocol.file.allow", "always"], env=env) + runner(["git", "config", "--global", "user.email", "test@yadm.io"], env=env) + runner(["git", "config", "--global", "user.name", "Yadm Test"], env=env) if submodule: ext_repo = tmpdir.mkdir("ext_repo") @@ -39,7 +41,7 @@ def test_upgrade(tmpdir, runner, versions, submodule): os.environ.pop("XDG_DATA_HOME", None) def run_version(version, *args, check_stderr=True): - yadm = f"yadm-{version}" if version else "/yadm/yadm" + yadm = f"yadm-{version}" if version else paths.pgm run = runner([yadm, *args], shell=True, cwd=str(home), env=env) assert run.success if check_stderr: From 216d49ceefe61b53e47f103dd9fecbea7f49b5bb Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 20:51:45 +0100 Subject: [PATCH 19/32] Run mandoc or groff instead of man.REAL to check man page Also fix all warnings reported by mandoc and apply some of the recommendations from https://liw.fi/manpages/. --- Makefile | 2 +- test/test_syntax.py | 9 +- yadm.1 | 194 ++++++++++++++++++++++---------------------- 3 files changed, 105 insertions(+), 100 deletions(-) diff --git a/Makefile b/Makefile index a28851fc..db485125 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ man-ps: @groff -man -Tps ./yadm.1 > yadm.ps yadm.md: yadm.1 - @groff -man -Tutf8 -Z ./yadm.1 | grotty -c | col -bx | sed 's/^[A-Z]/## &/g' | sed '/yadm(1)/d' > yadm.md + @groff -man -Tutf8 -Z ./yadm.1 | grotty -c | col -bx | sed 's/^[A-Z]/## &/g' | sed '/YADM(1)/d' > yadm.md .PHONY: contrib contrib: SHELL = /bin/bash diff --git a/test/test_syntax.py b/test/test_syntax.py index 65498224..9d79499e 100644 --- a/test/test_syntax.py +++ b/test/test_syntax.py @@ -1,6 +1,7 @@ """Syntax checks""" import os +import shutil import pytest @@ -77,7 +78,11 @@ def test_yamllint(pytestconfig, runner, yamllint_version): def test_man(runner): """Check for warnings from man""" - run = runner(command=["man.REAL", "--warnings", "./yadm.1"]) + if shutil.which("mandoc"): + command = ["mandoc", "-T", "lint"] + else: + command = ["groff", "-ww", "-z"] + run = runner(command=command + ["-man", "./yadm.1"]) assert run.success + assert run.out == "" assert run.err == "" - assert "yadm - Yet Another Dotfiles Manager" in run.out diff --git a/yadm.1 b/yadm.1 index 1f36498a..2cc70301 100644 --- a/yadm.1 +++ b/yadm.1 @@ -1,5 +1,5 @@ .\" vim: set spell so=8: -.TH yadm 1 "8 November 2024" "3.3.0" +.TH YADM 1 "November 8, 2024" "3.3.0" .SH NAME @@ -15,55 +15,55 @@ yadm \- Yet Another Dotfiles Manager .I git-command-or-alias .RI [ options ] -.B yadm -init -.RB [ -f ] -.RB [ -w +.B yadm init +.RB [ \-f ] +.RB [ \-w .IR dir ] -.B yadm -.RI clone " url -.RB [ -f ] -.RB [ -w +.B yadm clone +.I url +.RB [ \-f ] +.RB [ \-w .IR dir ] -.RB [ -b +.RB [ \-b .IR branch ] -.RB [ --bootstrap ] -.RB [ --no-bootstrap ] +.RB [ \-\-bootstrap ] +.RB [ \-\-no\-bootstrap ] -.B yadm -.RI config " name +.B yadm config +.I name .RI [ value ] -.B yadm -config -.RB [ -e ] +.B yadm config +.RB [ \-e ] -.B yadm -list -.RB [ -a ] +.B yadm list +.RB [ \-a ] -.BR yadm " bootstrap +.B yadm bootstrap -.BR yadm " encrypt +.B yadm encrypt -.BR yadm " decrypt -.RB [ -l ] +.B yadm decrypt +.RB [ \-l ] -.BR yadm " alt +.B yadm alt -.BR yadm " perms +.B yadm perms -.BR yadm " enter [ command ] +.B yadm enter +.RI [ command ] -.BR yadm " git-crypt [ options ] +.B yadm git\-crypt +.RI [ options ] -.BR yadm " transcrypt [ options ] +.B yadm transcrypt +.RI [ options ] -.BR yadm " upgrade -.RB [ -f ] +.B yadm upgrade +.RB [ \-f ] -.BR yadm " introspect +.B yadm introspect .I category .SH DESCRIPTION @@ -83,8 +83,7 @@ Any command not internally handled by yadm is passed through to .BR git (1). Git commands or aliases are invoked with the yadm managed repository. The working directory for Git commands will be the configured -.IR work-tree " (usually -.IR $HOME ). +.IR work-tree \ (usually\ $HOME ). Dotfiles are managed by using standard .B git @@ -95,7 +94,7 @@ commands; .IR pull , etc. -.RI The " config +.RI The\ config command is not passed directly through. Instead use the .I gitconfig @@ -114,7 +113,7 @@ Execute .I $HOME/.config/yadm/bootstrap if it exists. .TP -.BI clone " url +.BI clone \ url Clone a remote repository for tracking dotfiles. After the contents of the remote repository have been fetched, a "check out" of the remote HEAD branch is attempted. @@ -130,15 +129,15 @@ By default, will be used as the .IR work-tree , but this can be overridden with the -.BR -w " option. +.BR \-w \ option. yadm can be forced to overwrite an existing repository by providing the -.BR -f " option. +.BR \-f \ option. If you want to use a branch other than the remote HEAD branch you can specify it using the -.BR -b " option. +.BR \-b \ option. By default yadm will ask the user if the bootstrap program should be run (if it exists). The options -.BR --bootstrap " or " --no-bootstrap +.BR \-\-bootstrap " or " \-\-no\-bootstrap will either force the bootstrap to be run, or prevent it from being run, without prompting the user. .TP @@ -153,10 +152,9 @@ See the CONFIGURATION section for more details. Decrypt all files stored in .IR $HOME/.local/share/yadm/archive . Files decrypted will be relative to the configured -.IR work-tree " (usually -.IR $HOME ). +.IR work-tree \ (usually\ $HOME ). Using the -.B -l +.B \-l option will list the files stored without extracting them. .TP .B encrypt @@ -191,12 +189,12 @@ Emacs Tramp and Magit can manage files by using this configuration: With this config, use (magit-status "/yadm::"). .RE .TP -.BI git-crypt " options +.BI git-crypt \ options If git-crypt is installed, this command allows you to pass options directly to git-crypt, with the environment configured to use the yadm repository. -git-crypt enables transparent encryption and decryption of files in a git repository. -You can read +git-crypt enables transparent encryption and decryption of files in a git +repository. You can read https://github.com/AGWA/git-crypt for details. .TP @@ -232,17 +230,17 @@ By default, will be used as the .IR work-tree , but this can be overridden with the -.BR -w " option. +.BR \-w \ option. yadm can be forced to overwrite an existing repository by providing the -.BR -f " option. +.BR \-f \ option. .TP .B list Print a list of files managed by yadm. -.RB The " -a +.RB The \ \-a option will cause all managed files to be listed. Otherwise, the list will only include files from the current directory or below. .TP -.BI introspect " category +.BI introspect \ category Report internal yadm data. Supported categories are .IR commands , .IR configs , @@ -259,12 +257,12 @@ configuration .I yadm.auto-perms to "false". .TP -.BI transcrypt " options +.BI transcrypt \ options If transcrypt is installed, this command allows you to pass options directly to transcrypt, with the environment configured to use the yadm repository. -transcrypt enables transparent encryption and decryption of files in a git repository. -You can read +transcrypt enables transparent encryption and decryption of files in a git +repository. You can read https://github.com/elasticdog/transcrypt for details. .TP @@ -283,7 +281,7 @@ your submodules cannot be de-initialized, the upgrade will fail. The most common reason submodules will fail to de-initialize is because they have local modifications. If you are willing to lose the local modifications to those submodules, you can use the -.B -f +.B \-f option with the "upgrade" command to force the de-initialization. After running "yadm upgrade", you should run "yadm status" to review changes @@ -306,33 +304,33 @@ For example, the following alias could be used to override the repository directory. .RS -alias yadm='yadm --yadm-repo /alternate/path/to/repo' +alias yadm='yadm \-\-yadm\-repo /alternate/path/to/repo' .RE The following is the full list of universal options. Each option should be followed by a path. .TP -.B -Y,--yadm-dir +.B \-Y, \-\-yadm\-dir Override the yadm directory. yadm stores its configurations relative to this directory. .TP -.B --yadm-data +.B \-\-yadm\-data Override the yadm data directory. yadm stores its data relative to this directory. .TP -.B --yadm-repo +.B \-\-yadm\-repo Override the location of the yadm repository. .TP -.B --yadm-config +.B \-\-yadm\-config Override the location of the yadm configuration file. .TP -.B --yadm-encrypt +.B \-\-yadm\-encrypt Override the location of the yadm encryption configuration. .TP -.B --yadm-archive +.B \-\-yadm\-archive Override the location of the yadm encrypted files archive. .TP -.B --yadm-bootstrap +.B \-\-yadm\-bootstrap Override the location of the yadm bootstrap program. .SH CONFIGURATION @@ -377,7 +375,8 @@ manually to update permissions. This feature is enabled by default. .TP .B yadm.auto-private-dirs -Disable the automatic creating of private directories described in the section PERMISSIONS. +Disable the automatic creating of private directories described in the section +PERMISSIONS. .TP .B yadm.cipher Configure which encryption system is used by the encrypt/decrypt commands. @@ -426,7 +425,7 @@ Disable the permission changes to .IR $HOME/.ssh/* . This feature is enabled by default. -.RE +.LP The following "local" configurations are not stored in the .IR $HOME/.config/yadm/config, they are stored in the local repository. @@ -438,7 +437,7 @@ By default, no class will be matched. The local host can be assigned multiple classes using command: .RS -yadm config --add local.class +yadm config \-\-add local.class .RE .TP .B local.arch @@ -486,22 +485,22 @@ Value is compared case-insensitive. These are the supported attributes, in the order of the weighted precedence: .TP -.BR template , " t +.BR template ,\ t Valid when the value matches a supported template processor. See the TEMPLATES section for more details. .TP -.BR user , " u +.BR user ,\ u Valid if the value matches the current user. Current user is calculated by running -.BR "id -u -n" . +.BR "id \-u \-n" . .TP -.BR hostname , " h +.BR hostname ,\ h Valid if the value matches the short hostname. Hostname is calculated by running -.BR "uname -n" , +.BR "uname \-n" , and trimming off any domain. .TP -.BR class , " c +.BR class ,\ c Valid if the value matches the .B local.class configuration. @@ -510,38 +509,38 @@ Class must be manually set using See the CONFIGURATION section for more details about setting .BR local.class . .TP -.BR distro , " d +.BR distro ,\ d Valid if the value matches the distro. Distro is calculated by running -.B "lsb_release -si" +.B "lsb_release \-si" or by inspecting the ID from .BR "/etc/os-release" . .TP -.BR distro_family , " f +.BR distro_family ,\ f Valid if the value matches the distro family. Distro family is calculated by inspecting the ID_LIKE line from .B "/etc/os-release" (or ID if no ID_LIKE line is found). .TP -.BR os , " o +.BR os ,\ o Valid if the value matches the OS. OS is calculated by running -.BR "uname -s" . +.BR "uname \-s" . .TP -.BR arch , " a +.BR arch ,\ a Valid if the value matches the architecture. Architecture is calculated by running -.BR "uname -m" . +.BR "uname \-m" . .TP .B default Valid when no other alternate is valid. .TP -.BR extension , " e +.BR extension ,\ e A special "condition" that doesn't affect the selection process. Its purpose is instead to allow the alternate file to end with a certain extension to e.g. make editors highlight the content properly. -.LP +.LP .BR NOTE : The OS for "Windows Subsystem for Linux" is reported as "WSL", even though uname identifies as "Linux". @@ -587,7 +586,8 @@ which looks like this: .IR $HOME/path/example.txt " -> " $HOME/path/example.txt##os.Darwin -Since the hostname doesn't match any of the managed files, the more generic version is chosen. +Since the hostname doesn't match any of the managed files, the more generic +version is chosen. If running on a Linux server named "host4", the link will be: @@ -667,9 +667,10 @@ To use the j2cli Jinja template processor, specify the value of "j2" or "j2cli". .TP .B envtpl -To use the envtpl Jinja template processor, specify the value of "j2" or "envtpl". -.LP +To use the envtpl Jinja template processor, specify the value of "j2" or +"envtpl". +.LP .BR NOTE : Specifying "j2" as the processor will attempt to use j2cli or envtpl, whichever is available. @@ -681,15 +682,15 @@ During processing, the following variables are available in the template: Default Jinja or ESH Description ------------- ------------- ---------------------------- - yadm.arch YADM_ARCH uname -m + yadm.arch YADM_ARCH uname \-m yadm.class YADM_CLASS Last locally defined class yadm.classes YADM_CLASSES All classes - yadm.distro YADM_DISTRO lsb_release -si + yadm.distro YADM_DISTRO lsb_release \-si yadm.distro_family YADM_DISTRO_FAMILY ID_LIKE from /etc/os-release - yadm.hostname YADM_HOSTNAME uname -n (without domain) - yadm.os YADM_OS uname -s + yadm.hostname YADM_HOSTNAME uname \-n (without domain) + yadm.os YADM_OS uname \-s yadm.source YADM_SOURCE Template filename - yadm.user YADM_USER id -u -n + yadm.user YADM_USER id \-u \-n env.VAR Environment variable VAR .BR NOTE : @@ -767,8 +768,7 @@ configuration. To use this feature, a list of patterns must be created and saved as .IR $HOME/.config/yadm/encrypt . This list of patterns should be relative to the configured -.IR work-tree " (usually -.IR $HOME ). +.IR work-tree \ (usually\ $HOME ). For example: .RS @@ -785,11 +785,12 @@ Paths beginning with a "!" will be excluded. The .B yadm encrypt -command will find all files matching the patterns, and prompt for a password. Once a -password has confirmed, the matching files will be encrypted and saved as +command will find all files matching the patterns, and prompt for a +password. Once a password has confirmed, the matching files will be encrypted +and saved as .IR $HOME/.local/share/yadm/archive . -The "encrypt" and "archive" files should be added to the yadm repository so they are -available across multiple systems. +The "encrypt" and "archive" files should be added to the yadm repository so +they are available across multiple systems. To decrypt these files later, or on another system run .B yadm decrypt @@ -832,7 +833,6 @@ repository. See the following web sites for more information: - https://github.com/elasticdog/transcrypt - https://github.com/AGWA/git-crypt -.LP .SH PERMISSIONS @@ -841,7 +841,7 @@ dependent upon the user's umask. Because of this, yadm will automatically update the permissions of some file paths. The "group" and "others" permissions will be removed from the following files: -.RI - " $HOME/.local/share/yadm/archive +.RI -\ $HOME/.local/share/yadm/archive - All files matching patterns in .I $HOME/.config/yadm/encrypt @@ -991,7 +991,7 @@ to the Git index and create a new commit .B yadm remote add origin Add a remote origin to an existing repository .TP -.B yadm push -u origin master +.B yadm push \-u origin master Initial push of master to origin .TP .B echo ".ssh/*.key" >> $HOME/.config/yadm/encrypt From 4511f5d9c638d454f0f3d2835f7594403018264f Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 22:56:07 +0100 Subject: [PATCH 20/32] Use git ls-files to list files to encrypt By using git ls-files instead of bash we can support ** also on macOS where the included bash version (3) doesn't support globstar. --- test/test_unit_parse_encrypt.py | 7 ++-- yadm | 68 +++++++++------------------------ yadm.1 | 12 +++--- 3 files changed, 28 insertions(+), 59 deletions(-) diff --git a/test/test_unit_parse_encrypt.py b/test/test_unit_parse_encrypt.py index 6a5c23bf..d7db41a0 100644 --- a/test/test_unit_parse_encrypt.py +++ b/test/test_unit_parse_encrypt.py @@ -105,7 +105,8 @@ def create_test_encrypt_data(paths): edata += "dirwild*\n" paths.work.join("dirwildcard/file1").write("", ensure=True) paths.work.join("dirwildcard/file2").write("", ensure=True) - expected.add("dirwildcard") + expected.add("dirwildcard/file1") + expected.add("dirwildcard/file2") # excludes edata += "exclude*\n" @@ -186,9 +187,7 @@ def run_parse_encrypt(runner, paths, skip_parse=False, twice=False): YADM_WORK={paths.work} export YADM_WORK {parse_cmd} - export ENCRYPT_INCLUDE_FILES - export PARSE_ENCRYPT_SHORT - env + echo PARSE_ENCRYPT_SHORT=$PARSE_ENCRYPT_SHORT echo EIF_COUNT:${{#ENCRYPT_INCLUDE_FILES[@]}} for value in "${{ENCRYPT_INCLUDE_FILES[@]}}"; do echo "EIF:$value" diff --git a/yadm b/yadm index 7b68f546..36b15ea1 100755 --- a/yadm +++ b/yadm @@ -1906,65 +1906,35 @@ function parse_encrypt() { fi ENCRYPT_INCLUDE_FILES=() - ENCRYPT_EXCLUDE_FILES=() - FINAL_INCLUDE=() [ -f "$YADM_ENCRYPT" ] || return cd_work "Parsing encrypt" || return - # setting globstar to allow ** in encrypt patterns - # (only supported on Bash >= 4) - local unset_globstar - if ! shopt globstar &> /dev/null; then - unset_globstar=1 - fi - shopt -s globstar &> /dev/null + local -a exclude + local -a include - exclude_pattern="^!(.+)" - # parse both included/excluded - while IFS='' read -r line || [ -n "$line" ]; do - if [[ ! $line =~ ^# && ! $line =~ ^[[:blank:]]*$ ]] ; then - local IFS=$'\n' - for pattern in $line; do - if [[ "$pattern" =~ $exclude_pattern ]]; then - for ex_file in ${BASH_REMATCH[1]}; do - if [ -e "$ex_file" ]; then - ENCRYPT_EXCLUDE_FILES+=("$ex_file") - fi - done - else - for in_file in $pattern; do - if [ -e "$in_file" ]; then - ENCRYPT_INCLUDE_FILES+=("$in_file") - fi - done + while IFS= read -r pattern; do + case $pattern in + \#*) + # Ignore comments + ;; + !*) + exclude+=("--exclude=${pattern:1}") + ;; + *) + if ! [[ $pattern =~ ^[[:blank:]]*$ ]]; then + include+=("$pattern") fi - done - fi + ;; + esac done < "$YADM_ENCRYPT" - # remove excludes from the includes - #(SC2068 is disabled because in this case, we desire globbing) - #shellcheck disable=SC2068 - for included in "${ENCRYPT_INCLUDE_FILES[@]}"; do - skip= - #shellcheck disable=SC2068 - for ex_file in ${ENCRYPT_EXCLUDE_FILES[@]}; do - [ "$included" == "$ex_file" ] && { skip=1; break; } - done - [ -n "$skip" ] || FINAL_INCLUDE+=("$included") - done - - # sort the encrypted files - #shellcheck disable=SC2207 - IFS=$'\n' ENCRYPT_INCLUDE_FILES=($(LC_ALL=C sort <<<"${FINAL_INCLUDE[*]}")) - unset IFS - - if [ "$unset_globstar" = "1" ]; then - shopt -u globstar &> /dev/null + if [[ ${#include} -gt 0 ]]; then + while IFS= read -r filename; do + ENCRYPT_INCLUDE_FILES+=("${filename%/}") + done <<< "$("$GIT_PROGRAM" ls-files --others "${exclude[@]}" -- "${include[@]}")" fi - } function builtin_dirname() { diff --git a/yadm.1 b/yadm.1 index 2cc70301..f16a34e3 100644 --- a/yadm.1 +++ b/yadm.1 @@ -765,7 +765,8 @@ gpg is used by default, but openssl can be configured with the .I yadm.cipher configuration. -To use this feature, a list of patterns must be created and saved as +To use this feature, a list of patterns (one per line) must be created and +saved as .IR $HOME/.config/yadm/encrypt . This list of patterns should be relative to the configured .IR work-tree \ (usually\ $HOME ). @@ -776,11 +777,10 @@ For example: .gnupg/*.gpg .RE -Standard filename expansions (*, ?, [) are supported. -If you have Bash version 4, you may use "**" to match all subdirectories. -Other shell expansions like brace and tilde are not supported. -Spaces in paths are supported, and should not be quoted. -If a directory is specified, its contents will be included, but not recursively. +Standard filename expansions (*, ?, [) are supported. Two consecutive asterisks +"**" can be used to match all subdirectories. Other shell expansions like +brace and tilde are not supported. Spaces in paths are supported, and should +not be quoted. If a directory is specified, its contents will be included. Paths beginning with a "!" will be excluded. The From bb21c9a267362b654e57b3118b237f921503afa2 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 24 Nov 2024 20:58:17 +0100 Subject: [PATCH 21/32] Run tests directly on github runner instead of in the docker image As this makes it possible to run the tests on different systems. Initially ubuntu (20.04 and 24.04) and macOs (13 and 15). --- .github/workflows/test.yml | 93 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1dae7cff..7b5f7888 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,13 +1,98 @@ --- name: Tests + on: # yamllint disable-line rule:truthy - push - pull_request - workflow_dispatch + +env: + SC_VER: "0.10.0" + ESH_VER: "0.3.2" + jobs: Tests: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-20.04 + - ubuntu-24.04 + - macos-13 + - macos-15 steps: - - uses: actions/checkout@v2 - - name: Tests - run: make test + - uses: actions/checkout@v4 + + - name: Install dependencies on Linux + if: ${{ runner.os == 'Linux' }} + run: | + sudo apt-get update + sudo apt-get install -y expect + if [ "${{ matrix.os }}" != "ubuntu-20.04" ]; then + sudo apt-get install -y j2cli + fi + + - name: Install dependencies on macOS + if: ${{ runner.os == 'macOS' }} + run: | + command -v expect || brew install expect + + - name: Prepare tools directory + run: | + mkdir "$RUNNER_TEMP/tools" + echo "$RUNNER_TEMP/tools" >> "$GITHUB_PATH" + + - name: Install shellcheck + run: | + if [ "$RUNNER_OS" = "macOS" ]; then + OS=darwin + else + OS=linux + fi + + if [ "$RUNNER_ARCH" = "ARM64" ]; then + ARCH=aarch64 + else + ARCH=x86_64 + fi + + cd "$RUNNER_TEMP" + + BASE_URL="https://github.com/koalaman/shellcheck/releases/download" + SC="v$SC_VER/shellcheck-v$SC_VER.$OS.$ARCH.tar.xz" + curl -L "$BASE_URL/$SC" | tar Jx shellcheck-v$SC_VER/shellcheck + mv shellcheck-v$SC_VER/shellcheck tools + + - name: Install esh + run: | + cd "$RUNNER_TEMP/tools" + + BASE_URL="https://github.com/jirutka/esh/raw/refs/tags" + curl -L -o esh "$BASE_URL/v$ESH_VER/esh" + chmod +x esh + + - name: Add old yadm versions # to test upgrades + run: | + for version in 1.12.0 2.5.0; do + git fetch origin $version:refs/tags/$version + git cat-file blob $version:yadm > "$RUNNER_TEMP/tools/yadm-$version" + chmod +x "$RUNNER_TEMP/tools/yadm-$version" + done + + - name: Set up Python 3.11 + if: ${{ runner.os == 'macOS' || matrix.os == 'ubuntu-20.04' }} + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r test/requirements.txt + + - name: Run tests + run: | + git config --global user.email test@yadm.io + git config --global user.name "Yadm Test" + pytest -v --color=yes --basetemp="$RUNNER_TEMP/pytest" From f5dfc7ab019b3b6c9680049d15a47f41bacea805 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Wed, 11 Dec 2024 20:05:16 +0100 Subject: [PATCH 22/32] Format yadm and bootstrap with shfmt Command: shfmt -w -ln bash -i 2 -ci --- bootstrap | 16 ++- yadm | 423 +++++++++++++++++++++++++++--------------------------- 2 files changed, 218 insertions(+), 221 deletions(-) diff --git a/bootstrap b/bootstrap index 70a96877..47646f7c 100755 --- a/bootstrap +++ b/bootstrap @@ -35,18 +35,20 @@ REPO_URL="" function _private_yadm() { unset -f yadm - if command -v yadm &> /dev/null; then + if command -v yadm &>/dev/null; then echo "Found yadm installed locally, removing remote yadm() function" unset -f _private_yadm command yadm "$@" else - function yadm() { _private_yadm "$@"; }; export -f yadm + function yadm() { _private_yadm "$@"; } + export -f yadm echo WARNING: Using yadm remotely. You should install yadm locally. curl -fsSL "$YADM_REPO/raw/$YADM_RELEASE/yadm" | bash -s -- "$@" fi } export -f _private_yadm -function yadm() { _private_yadm "$@"; }; export -f yadm +function yadm() { _private_yadm "$@"; } +export -f yadm # if being sourced, return here, otherwise continue processing return 2>/dev/null @@ -57,7 +59,7 @@ function remote_yadm() { } function ask_about_source() { - if ! command -v yadm &> /dev/null; then + if ! command -v yadm &>/dev/null; then echo echo "***************************************************" echo "yadm is NOT currently installed." @@ -83,7 +85,7 @@ function build_url() { echo " 3. GitLab" echo " 4. Other" echo - read -r -p "Where is your repo? (1/2/3/4) ->" choice < /dev/tty + read -r -p "Where is your repo? (1/2/3/4) ->" choice " choice < /dev/tty + read -r -p "URL ->" choice " choice < /dev/tty + read -r -p "User/Repo ->" choice = 0; --index )); do + for (( ; index >= 0; --index)); do if [ "${alt_targets[$index]}" = "$target" ]; then break fi @@ -259,11 +258,11 @@ function record_score() { if [ $index -lt 0 ]; then # $YADM_CONFIG must be processed first, in case other templates lookup yadm configurations if [ "$target" = "$YADM_CONFIG" ]; then - alt_targets=("$target" "${alt_targets[@]}") + alt_targets=("$target" "${alt_targets[@]}") - alt_sources=("$source" "${alt_sources[@]}") - alt_scores=("$score" "${alt_scores[@]}") - alt_template_cmds=("$template_cmd" "${alt_template_cmds[@]}") + alt_sources=("$source" "${alt_sources[@]}") + alt_scores=("$score" "${alt_scores[@]}") + alt_template_cmds=("$template_cmd" "${alt_template_cmds[@]}") else alt_targets+=("$target") @@ -314,7 +313,7 @@ function template_default() { # the explicit "space + tab" character class used below is used because not # all versions of awk seem to support the POSIX character classes [[:blank:]] - read -r -d '' awk_pgm << "EOF" + read -r -d '' awk_pgm <<"EOF" BEGIN { classes = ARGV[2] for (i = 3; i < ARGC; ++i) { @@ -437,7 +436,7 @@ EOF -v source="$input" \ -v source_dir="$(builtin_dirname "$input")" \ "$awk_pgm" \ - "$input" "${local_classes[@]}" > "$temp_file" || rm -f "$temp_file" + "$input" "${local_classes[@]}" >"$temp_file" || rm -f "$temp_file" move_file "$input" "$output" "$temp_file" } @@ -447,16 +446,16 @@ function template_j2cli() { output="$2" temp_file="${output}.$$.$RANDOM" - YADM_CLASS="$local_class" \ - YADM_ARCH="$local_arch" \ - YADM_OS="$local_system" \ - YADM_HOSTNAME="$local_host" \ - YADM_USER="$local_user" \ - YADM_DISTRO="$local_distro" \ - YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input" \ - YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$J2CLI_PROGRAM" "$input" -o "$temp_file" + YADM_CLASS="$local_class" \ + YADM_ARCH="$local_arch" \ + YADM_OS="$local_system" \ + YADM_HOSTNAME="$local_host" \ + YADM_USER="$local_user" \ + YADM_DISTRO="$local_distro" \ + YADM_DISTRO_FAMILY="$local_distro_family" \ + YADM_SOURCE="$input" \ + YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ + "$J2CLI_PROGRAM" "$input" -o "$temp_file" move_file "$input" "$output" "$temp_file" } @@ -466,16 +465,16 @@ function template_envtpl() { output="$2" temp_file="${output}.$$.$RANDOM" - YADM_CLASS="$local_class" \ - YADM_ARCH="$local_arch" \ - YADM_OS="$local_system" \ - YADM_HOSTNAME="$local_host" \ - YADM_USER="$local_user" \ - YADM_DISTRO="$local_distro" \ - YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input" \ - YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$ENVTPL_PROGRAM" --keep-template "$input" -o "$temp_file" + YADM_CLASS="$local_class" \ + YADM_ARCH="$local_arch" \ + YADM_OS="$local_system" \ + YADM_HOSTNAME="$local_host" \ + YADM_USER="$local_user" \ + YADM_DISTRO="$local_distro" \ + YADM_DISTRO_FAMILY="$local_distro_family" \ + YADM_SOURCE="$input" \ + YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ + "$ENVTPL_PROGRAM" --keep-template "$input" -o "$temp_file" move_file "$input" "$output" "$temp_file" } @@ -486,15 +485,15 @@ function template_esh() { temp_file="${output}.$$.$RANDOM" YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$ESH_PROGRAM" -o "$temp_file" "$input" \ - YADM_CLASS="$local_class" \ - YADM_ARCH="$local_arch" \ - YADM_OS="$local_system" \ - YADM_HOSTNAME="$local_host" \ - YADM_USER="$local_user" \ - YADM_DISTRO="$local_distro" \ - YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input" + "$ESH_PROGRAM" -o "$temp_file" "$input" \ + YADM_CLASS="$local_class" \ + YADM_ARCH="$local_arch" \ + YADM_OS="$local_system" \ + YADM_HOSTNAME="$local_host" \ + YADM_USER="$local_user" \ + YADM_DISTRO="$local_distro" \ + YADM_DISTRO_FAMILY="$local_distro_family" \ + YADM_SOURCE="$input" move_file "$input" "$output" "$temp_file" } @@ -617,7 +616,7 @@ function report_invalid_alts() { ${path_list} *********** EOF - printf '%s\n' "$msg" >&2 + printf '%s\n' "$msg" >&2 } function remove_stale_links() { @@ -646,7 +645,7 @@ function set_local_alt_values() { while IFS='' read -r class; do local_classes+=("$class") local_class="$class" - done <<< "$all_classes" + done <<<"$all_classes" local_arch="$(config local.arch)" if [[ -z "$local_arch" ]]; then @@ -683,7 +682,7 @@ function set_local_alt_values() { function alt_linking() { local -i index - for (( index = 0; index < ${#alt_targets[@]}; ++index )); do + for ((index = 0; index < ${#alt_targets[@]}; ++index)); do local target="${alt_targets[$index]}" local source="${alt_sources[$index]}" local template_cmd="${alt_template_cmds[$index]}" @@ -750,30 +749,30 @@ function clone() { DO_BOOTSTRAP=1 local -a args local -i do_checkout=1 - while [[ $# -gt 0 ]] ; do + while [[ $# -gt 0 ]]; do case "$1" in --bootstrap) # force bootstrap, without prompt DO_BOOTSTRAP=2 - ;; + ;; --no-bootstrap) # prevent bootstrap, without prompt DO_BOOTSTRAP=3 - ;; + ;; --checkout) do_checkout=1 - ;; - -n|--no-checkout) + ;; + -n | --no-checkout) do_checkout=0 - ;; - --bare|--mirror|--recurse-submodules*|--recursive|--separate-git-dir=*) + ;; + --bare | --mirror | --recurse-submodules* | --recursive | --separate-git-dir=*) # ignore arguments without separate parameter - ;; + ;; --separate-git-dir) # ignore arguments with separate parameter shift - ;; + ;; *) args+=("$1") - ;; + ;; esac shift done @@ -798,11 +797,11 @@ function clone() { # first clone without checkout debug "Doing an initial clone of the repository" (cd "$wc" && - "$GIT_PROGRAM" -c core.sharedrepository=0600 clone --no-checkout \ - --separate-git-dir="$YADM_REPO" "${args[@]}" repo.git) || { - debug "Removing repo after failed clone" - rm -rf "$YADM_REPO" "$wc" - error_out "Unable to clone the repository" + "$GIT_PROGRAM" -c core.sharedrepository=0600 clone --no-checkout \ + --separate-git-dir="$YADM_REPO" "${args[@]}" repo.git) || { + debug "Removing repo after failed clone" + rm -rf "$YADM_REPO" "$wc" + error_out "Unable to clone the repository" } configure_repo rm -rf "$wc" @@ -823,17 +822,17 @@ function clone() { # finally check out (unless instructed not to) all files that don't exist in $YADM_WORK if [[ $do_checkout -ne 0 ]]; then - [ -n "$DEBUG" ] && display_private_perms "pre-checkout" + [ -n "$DEBUG" ] && display_private_perms "pre-checkout" - cd_work "Clone" || return + cd_work "Clone" || return - "$GIT_PROGRAM" ls-files --deleted | while IFS= read -r file; do - "$GIT_PROGRAM" checkout -- ":/$file" - done + "$GIT_PROGRAM" ls-files --deleted | while IFS= read -r file; do + "$GIT_PROGRAM" checkout -- ":/$file" + done - if [ -n "$("$GIT_PROGRAM" ls-files --modified)" ]; then - local msg - IFS='' read -r -d '' msg </dev/null) archive_regex="^\?\?" - if [[ $archive_status =~ $archive_regex ]] ; then + if [[ $archive_status =~ $archive_regex ]]; then echo "It appears that $YADM_ARCHIVE is not tracked by yadm's repository." echo "Would you like to add it now? (y/n)" - read -r answer < /dev/tty - if [[ $answer =~ ^[yY]$ ]] ; then + read -r answer [options...] Manage dotfiles maintained in a Git repository. Manage alternate files @@ -1201,9 +1200,9 @@ function init() { function introspect() { case "$1" in - commands|configs|repo|switches) + commands | configs | repo | switches) "introspect_$1" - ;; + ;; esac } @@ -1284,7 +1283,7 @@ function list() { require_repo # process relative to YADM_WORK when --all is specified - if [ -n "$LIST_ALL" ] ; then + if [ -n "$LIST_ALL" ]; then cd_work "List" || return fi @@ -1309,13 +1308,13 @@ function perms() { # only include private globs if using HOME as worktree if [ "$YADM_WORK" = "$HOME" ]; then # include all .ssh files (unless disabled) - if [[ $(config --bool yadm.ssh-perms) != "false" ]] ; then + if [[ $(config --bool yadm.ssh-perms) != "false" ]]; then GLOBS+=(".ssh" ".ssh/*" ".ssh/.[!.]*") fi # include all gpg files (unless disabled) gnupghome="$(private_dirs gnupg)" - if [[ $(config --bool yadm.gpg-perms) != "false" ]] ; then + if [[ $(config --bool yadm.gpg-perms) != "false" ]]; then GLOBS+=("${gnupghome}" "${gnupghome}/*" "${gnupghome}/.[!.]*") fi fi @@ -1326,7 +1325,7 @@ function perms() { # remove group/other permissions from collected globs #shellcheck disable=SC2068 #(SC2068 is disabled because in this case, we desire globbing) - chmod -f go-rwx ${GLOBS[@]} &> /dev/null + chmod -f go-rwx ${GLOBS[@]} &>/dev/null # TODO: detect and report changing permissions in a portable way } @@ -1337,7 +1336,7 @@ function upgrade() { local -a submodules local repo_updates=0 - [[ -n "${YADM_OVERRIDE_REPO}${YADM_OVERRIDE_ARCHIVE}" || "$YADM_DATA" = "$YADM_DIR" ]] && \ + [[ -n "${YADM_OVERRIDE_REPO}${YADM_OVERRIDE_ARCHIVE}" || "$YADM_DATA" = "$YADM_DIR" ]] && error_out "Unable to upgrade. Paths have been overridden with command line options" # choose a legacy repo, the version 2 location will be favored @@ -1364,18 +1363,18 @@ function upgrade() { local submodule_status submodule_status=$("$GIT_PROGRAM" -C "$YADM_WORK" submodule status) while read -r sha submodule rest; do - [ "$submodule" == "" ] && continue - if [[ "$sha" = -* ]]; then - continue - fi - "$GIT_PROGRAM" -C "$YADM_WORK" submodule deinit ${FORCE:+-f} -- "$submodule" || { - for other in "${submodules[@]}"; do - "$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$other" - done - error_out "Unable to upgrade. Could not deinit submodule $submodule" - } - submodules+=("$submodule") - done <<< "$submodule_status" + [ "$submodule" == "" ] && continue + if [[ "$sha" = -* ]]; then + continue + fi + "$GIT_PROGRAM" -C "$YADM_WORK" submodule deinit ${FORCE:+-f} -- "$submodule" || { + for other in "${submodules[@]}"; do + "$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$other" + done + error_out "Unable to upgrade. Could not deinit submodule $submodule" + } + submodules+=("$submodule") + done <<<"$submodule_status" assert_parent "$YADM_REPO" mv "$LEGACY_REPO" "$YADM_REPO" @@ -1395,7 +1394,7 @@ function upgrade() { echo "Moving $LEGACY_ARCHIVE to $YADM_ARCHIVE" assert_parent "$YADM_ARCHIVE" # test to see if path is "tracked" in repo, if so 'git mv' must be used - if "$GIT_PROGRAM" ls-files --error-unmatch "$LEGACY_ARCHIVE" &> /dev/null; then + if "$GIT_PROGRAM" ls-files --error-unmatch "$LEGACY_ARCHIVE" &>/dev/null; then "$GIT_PROGRAM" mv "$LEGACY_ARCHIVE" "$YADM_ARCHIVE" && repo_updates=1 else mv -i "$LEGACY_ARCHIVE" "$YADM_ARCHIVE" @@ -1403,13 +1402,11 @@ function upgrade() { fi # handle any remaining version 1 paths - for legacy_path in \ - "$YADM_LEGACY_DIR/config" \ - "$YADM_LEGACY_DIR/encrypt" \ - "$YADM_LEGACY_DIR/bootstrap" \ - "$YADM_LEGACY_DIR"/hooks/{pre,post}_* \ - ; - do + for legacy_path in \ + "$YADM_LEGACY_DIR/config" \ + "$YADM_LEGACY_DIR/encrypt" \ + "$YADM_LEGACY_DIR/bootstrap" \ + "$YADM_LEGACY_DIR"/hooks/{pre,post}_*; do if [ -e "$legacy_path" ]; then new_filename="${legacy_path#"$YADM_LEGACY_DIR/"}" new_filename="$YADM_DIR/$new_filename" @@ -1417,7 +1414,7 @@ function upgrade() { echo "Moving $legacy_path to $new_filename" assert_parent "$new_filename" # test to see if path is "tracked" in repo, if so 'git mv' must be used - if "$GIT_PROGRAM" ls-files --error-unmatch "$legacy_path" &> /dev/null; then + if "$GIT_PROGRAM" ls-files --error-unmatch "$legacy_path" &>/dev/null; then "$GIT_PROGRAM" mv "$legacy_path" "$new_filename" && repo_updates=1 else mv -i "$legacy_path" "$new_filename" @@ -1427,13 +1424,13 @@ function upgrade() { # handle submodules, which need to be reinitialized for submodule in "${submodules[@]}"; do - "$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$submodule" + "$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$submodule" done - [ "$actions_performed" -eq 0 ] && \ + [ "$actions_performed" -eq 0 ] && echo "No legacy paths found. Upgrade is not necessary" - [ "$repo_updates" -eq 1 ] && \ + [ "$repo_updates" -eq 1 ] && echo "Some files tracked by yadm have been renamed. These changes should probably be commited now." exit 0 @@ -1443,7 +1440,8 @@ function upgrade() { function version() { echo "bash version $BASH_VERSION" - printf " "; "$GIT_PROGRAM" --version + printf " " + "$GIT_PROGRAM" --version echo "yadm version $VERSION" exit_with_hook 0 @@ -1472,7 +1470,7 @@ function exclude_encrypted() { encrypt_data="" while IFS='' read -r line || [ -n "$line" ]; do encrypt_data="${encrypt_data}${line}${newline}" - done < "$YADM_ENCRYPT" + done <"$YADM_ENCRYPT" # read info/exclude unmanaged="" @@ -1486,13 +1484,13 @@ function exclude_encrypted() { else managed="${managed}${line}${newline}" fi - done < "$exclude_path" + done <"$exclude_path" fi if [ "${exclude_header}${encrypt_data}" != "$managed" ]; then debug "Updating ${exclude_path}" assert_parent "$exclude_path" - printf "%s" "${unmanaged}${exclude_header}${encrypt_data}" > "$exclude_path" + printf "%s" "${unmanaged}${exclude_header}${encrypt_data}" >"$exclude_path" fi return 0 @@ -1501,16 +1499,16 @@ function exclude_encrypted() { function query_distro() { local distro="" - if command -v "$LSB_RELEASE_PROGRAM" &> /dev/null; then + if command -v "$LSB_RELEASE_PROGRAM" &>/dev/null; then distro=$($LSB_RELEASE_PROGRAM -si 2>/dev/null) elif [ -f "$OS_RELEASE" ]; then while IFS='' read -r line || [ -n "$line" ]; do if [[ "$line" = ID=* ]]; then distro="${line#ID=}" - distro="${distro//\"}" + distro="${distro//\"/}" break fi - done < "$OS_RELEASE" + done <"$OS_RELEASE" fi echo "$distro" } @@ -1526,49 +1524,49 @@ function query_distro_family() { family="${line#ID=}" # No break, only used as fallback in case ID_LIKE isn't found fi - done < "$OS_RELEASE" + done <"$OS_RELEASE" fi - echo "${family//\"}" + echo "${family//\"/}" } function process_global_args() { # global arguments are removed before the main processing is done MAIN_ARGS=() - while [[ $# -gt 0 ]] ; do + while [[ $# -gt 0 ]]; do key="$1" case $key in - -Y|--yadm-dir) # override the standard YADM_DIR + -Y | --yadm-dir) # override the standard YADM_DIR YADM_DIR="$(qualify_path "$2" "yadm")" shift - ;; + ;; --yadm-data) # override the standard YADM_DATA YADM_DATA="$(qualify_path "$2" "data")" shift - ;; + ;; --yadm-repo) # override the standard YADM_REPO YADM_OVERRIDE_REPO="$(qualify_path "$2" "repo")" shift - ;; + ;; --yadm-config) # override the standard YADM_CONFIG YADM_OVERRIDE_CONFIG="$(qualify_path "$2" "config")" shift - ;; + ;; --yadm-encrypt) # override the standard YADM_ENCRYPT YADM_OVERRIDE_ENCRYPT="$(qualify_path "$2" "encrypt")" shift - ;; + ;; --yadm-archive) # override the standard YADM_ARCHIVE YADM_OVERRIDE_ARCHIVE="$(qualify_path "$2" "archive")" shift - ;; + ;; --yadm-bootstrap) # override the standard YADM_BOOTSTRAP YADM_OVERRIDE_BOOTSTRAP="$(qualify_path "$2" "bootstrap")" shift - ;; + ;; *) # main arguments are kept intact MAIN_ARGS+=("$1") - ;; + ;; esac shift done @@ -1576,17 +1574,17 @@ function process_global_args() { } function qualify_path() { - local path="$1" - if [ -z "$path" ]; then - error_out "You can't specify an empty $2 path" - fi + local path="$1" + if [ -z "$path" ]; then + error_out "You can't specify an empty $2 path" + fi - if [ "$path" = "." ]; then - path="$PWD" - elif [[ "$path" != /* ]]; then - path="$PWD/${path#./}" - fi - echo "$path" + if [ "$path" = "." ]; then + path="$PWD" + elif [[ "$path" != /* ]]; then + path="$PWD/${path#./}" + fi + echo "$path" } function set_yadm_dirs() { @@ -1594,7 +1592,7 @@ function set_yadm_dirs() { # only resolve YADM_DATA if it hasn't been provided already if [ -z "$YADM_DATA" ]; then local base_yadm_data="$XDG_DATA_HOME" - if [[ ! "$base_yadm_data" =~ ^/ ]] ; then + if [[ ! "$base_yadm_data" =~ ^/ ]]; then base_yadm_data="${HOME}/.local/share" fi YADM_DATA="${base_yadm_data}/yadm" @@ -1603,7 +1601,7 @@ function set_yadm_dirs() { # only resolve YADM_DIR if it hasn't been provided already if [ -z "$YADM_DIR" ]; then local base_yadm_dir="$XDG_CONFIG_HOME" - if [[ ! "$base_yadm_dir" =~ ^/ ]] ; then + if [[ ! "$base_yadm_dir" =~ ^/ ]]; then base_yadm_dir="${HOME}/.config" fi YADM_DIR="${base_yadm_dir}/yadm" @@ -1627,17 +1625,15 @@ function issue_legacy_path_warning() { # test for legacy paths local legacy_found=() # this is ordered by importance - for legacy_path in \ - "$YADM_DIR/$YADM_REPO" \ - "$YADM_DIR/$YADM_LEGACY_ARCHIVE" \ - "$YADM_LEGACY_DIR/$YADM_REPO" \ - "$YADM_LEGACY_DIR/$YADM_BOOTSTRAP" \ - "$YADM_LEGACY_DIR/$YADM_CONFIG" \ - "$YADM_LEGACY_DIR/$YADM_ENCRYPT" \ + for legacy_path in \ + "$YADM_DIR/$YADM_REPO" \ + "$YADM_DIR/$YADM_LEGACY_ARCHIVE" \ + "$YADM_LEGACY_DIR/$YADM_REPO" \ + "$YADM_LEGACY_DIR/$YADM_BOOTSTRAP" \ + "$YADM_LEGACY_DIR/$YADM_CONFIG" \ + "$YADM_LEGACY_DIR/$YADM_ENCRYPT" \ "$YADM_LEGACY_DIR/$YADM_HOOKS"/{pre,post}_* \ - "$YADM_LEGACY_DIR/$YADM_LEGACY_ARCHIVE" \ - ; - do + "$YADM_LEGACY_DIR/$YADM_LEGACY_ARCHIVE"; do [ -e "$legacy_path" ] && legacy_found+=("$legacy_path") done @@ -1674,7 +1670,7 @@ ${path_list} *********** EOF printf '%s\n' "$msg" >&2 -LEGACY_WARNING_ISSUED=1 + LEGACY_WARNING_ISSUED=1 } @@ -1756,15 +1752,14 @@ function set_operating_system() { fi 2>/dev/null case "$OPERATING_SYSTEM" in - CYGWIN*|MINGW*|MSYS*) + CYGWIN* | MINGW* | MSYS*) git_version="$("$GIT_PROGRAM" --version 2>/dev/null)" - if [[ "$git_version" =~ windows ]] ; then - USE_CYGPATH=1 + if [[ "$git_version" =~ windows ]]; then + USE_CYGPATH=1 fi OPERATING_SYSTEM=$(uname -o) ;; - *) - ;; + *) ;; esac } @@ -1772,7 +1767,7 @@ function set_operating_system() { function set_awk() { local pgm for pgm in "${AWK_PROGRAM[@]}"; do - command -v "$pgm" &> /dev/null && AWK_PROGRAM=("$pgm") && return + command -v "$pgm" &>/dev/null && AWK_PROGRAM=("$pgm") && return done } @@ -1802,8 +1797,8 @@ function invoke_hook() { exit_status="$2" hook_command="${YADM_HOOKS}/${mode}_$HOOK_COMMAND" - if [ -x "$hook_command" ] || \ - { [[ $OPERATING_SYSTEM == MINGW* ]] && [ -f "$hook_command" ] ;} ; then + if [ -x "$hook_command" ] || + { [[ $OPERATING_SYSTEM == MINGW* ]] && [ -f "$hook_command" ]; }; then debug "Invoking hook: $hook_command" # expose some internal data to all hooks @@ -1868,7 +1863,7 @@ function assert_private_dirs() { if [ ! -d "$YADM_WORK/$private_dir" ]; then debug "Creating $YADM_WORK/$private_dir" #shellcheck disable=SC2174 - mkdir -m 0700 -p "$YADM_WORK/$private_dir" &> /dev/null + mkdir -m 0700 -p "$YADM_WORK/$private_dir" &>/dev/null fi done } @@ -1928,12 +1923,12 @@ function parse_encrypt() { fi ;; esac - done < "$YADM_ENCRYPT" + done <"$YADM_ENCRYPT" if [[ ${#include} -gt 0 ]]; then - while IFS= read -r filename; do - ENCRYPT_INCLUDE_FILES+=("${filename%/}") - done <<< "$("$GIT_PROGRAM" ls-files --others "${exclude[@]}" -- "${include[@]}")" + while IFS= read -r filename; do + ENCRYPT_INCLUDE_FILES+=("${filename%/}") + done <<<"$("$GIT_PROGRAM" ls-files --others "${exclude[@]}" -- "${include[@]}")" fi } @@ -2000,9 +1995,9 @@ function relative_path() { function auto_alt() { # process alternates if there are possible changes - if [ "$CHANGES_POSSIBLE" = "1" ] ; then + if [ "$CHANGES_POSSIBLE" = "1" ]; then auto_alt=$(config --bool yadm.auto-alt) - if [ "$auto_alt" != "false" ] ; then + if [ "$auto_alt" != "false" ]; then [ -d "$YADM_REPO" ] && alt fi fi @@ -2012,9 +2007,9 @@ function auto_alt() { function auto_perms() { # process permissions if there are possible changes - if [ "$CHANGES_POSSIBLE" = "1" ] ; then + if [ "$CHANGES_POSSIBLE" = "1" ]; then auto_perms=$(config --bool yadm.auto-perms) - if [ "$auto_perms" != "false" ] ; then + if [ "$auto_perms" != "false" ]; then [ -d "$YADM_REPO" ] && perms fi fi @@ -2028,12 +2023,12 @@ function auto_bootstrap() { [ "$DO_BOOTSTRAP" -eq 0 ] && return [ "$DO_BOOTSTRAP" -eq 3 ] && return [ "$DO_BOOTSTRAP" -eq 2 ] && bootstrap - if [ "$DO_BOOTSTRAP" -eq 1 ] ; then + if [ "$DO_BOOTSTRAP" -eq 1 ]; then echo "Found $YADM_BOOTSTRAP" echo "It appears that a bootstrap program exists." echo "Would you like to execute it now? (y/n)" - read -r answer < /dev/tty - if [[ $answer =~ ^[yY]$ ]] ; then + read -r answer /dev/null) - if [ -z "$mode" ] ; then + if [ -z "$mode" ]; then # BSD-style mode=$(stat -f '%p' "$filename" 2>/dev/null) mode=${mode: -4} fi # only accept results if they are octal - if [[ ! $mode =~ ^[0-7]+$ ]] ; then + if [[ ! $mode =~ ^[0-7]+$ ]]; then mode="" fi @@ -2105,11 +2100,11 @@ function require_git() { local more_info="" - if [ "$alt_git" != "" ] ; then + if [ "$alt_git" != "" ]; then GIT_PROGRAM="$alt_git" more_info="\nThis command has been set via the yadm.git-program configuration." fi - command -v "$GIT_PROGRAM" &> /dev/null || + command -v "$GIT_PROGRAM" &>/dev/null || error_out "This functionality requires Git to be installed, but the command '$GIT_PROGRAM' cannot be located.$more_info" } function require_gpg() { @@ -2118,11 +2113,11 @@ function require_gpg() { local more_info="" - if [ "$alt_gpg" != "" ] ; then + if [ "$alt_gpg" != "" ]; then GPG_PROGRAM="$alt_gpg" more_info="\nThis command has been set via the yadm.gpg-program configuration." fi - command -v "$GPG_PROGRAM" &> /dev/null || + command -v "$GPG_PROGRAM" &>/dev/null || error_out "This functionality requires GPG to be installed, but the command '$GPG_PROGRAM' cannot be located.$more_info" } function require_openssl() { @@ -2131,11 +2126,11 @@ function require_openssl() { local more_info="" - if [ "$alt_openssl" != "" ] ; then + if [ "$alt_openssl" != "" ]; then OPENSSL_PROGRAM="$alt_openssl" more_info="\nThis command has been set via the yadm.openssl-program configuration." fi - command -v "$OPENSSL_PROGRAM" &> /dev/null || + command -v "$OPENSSL_PROGRAM" &>/dev/null || error_out "This functionality requires OpenSSL to be installed, but the command '$OPENSSL_PROGRAM' cannot be located.$more_info" } function require_repo() { @@ -2145,11 +2140,11 @@ function require_shell() { [ -x "$SHELL" ] || error_out "\$SHELL does not refer to an executable." } function require_git_crypt() { - command -v "$GIT_CRYPT_PROGRAM" &> /dev/null || + command -v "$GIT_CRYPT_PROGRAM" &>/dev/null || error_out "This functionality requires git-crypt to be installed, but the command '$GIT_CRYPT_PROGRAM' cannot be located." } function require_transcrypt() { - command -v "$TRANSCRYPT_PROGRAM" &> /dev/null || + command -v "$TRANSCRYPT_PROGRAM" &>/dev/null || error_out "This functionality requires transcrypt to be installed, but the command '$TRANSCRYPT_PROGRAM' cannot be located." } function bootstrap_available() { @@ -2157,23 +2152,23 @@ function bootstrap_available() { return 1 } function awk_available() { - command -v "${AWK_PROGRAM[0]}" &> /dev/null && return + command -v "${AWK_PROGRAM[0]}" &>/dev/null && return return 1 } function j2cli_available() { - command -v "$J2CLI_PROGRAM" &> /dev/null && return + command -v "$J2CLI_PROGRAM" &>/dev/null && return return 1 } function envtpl_available() { - command -v "$ENVTPL_PROGRAM" &> /dev/null && return + command -v "$ENVTPL_PROGRAM" &>/dev/null && return return 1 } function esh_available() { - command -v "$ESH_PROGRAM" &> /dev/null && return + command -v "$ESH_PROGRAM" &>/dev/null && return return 1 } function readlink_available() { - command -v "readlink" &> /dev/null && return + command -v "readlink" &>/dev/null && return return 1 } @@ -2181,7 +2176,7 @@ function readlink_available() { function unix_path() { # for paths used by bash/yadm - if [ "$USE_CYGPATH" = "1" ] ; then + if [ "$USE_CYGPATH" = "1" ]; then cygpath -u "$1" else echo "$1" @@ -2189,7 +2184,7 @@ function unix_path() { } function mixed_path() { # for paths used by Git - if [ "$USE_CYGPATH" = "1" ] ; then + if [ "$USE_CYGPATH" = "1" ]; then cygpath -m "$1" else echo "$1" @@ -2213,7 +2208,7 @@ function echo_e() { # ****** Main processing (when not unit testing) ****** -if [ "$YADM_TEST" != 1 ] ; then +if [ "$YADM_TEST" != 1 ]; then process_global_args "$@" set_operating_system set_awk From 7a4de1a2478ef6f91b0ea2e09373ab1e42e232b0 Mon Sep 17 00:00:00 2001 From: Ross Smith II Date: Thu, 12 Oct 2023 07:58:10 -0700 Subject: [PATCH 23/32] Always remove temp_file on failure, other cleanup --- yadm | 60 ++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/yadm b/yadm index 12f42d78..2a741e12 100755 --- a/yadm +++ b/yadm @@ -364,6 +364,7 @@ function template_default() { input="$1" output="$2" + local awk_pgm # the explicit "space + tab" character class used below is used because not # all versions of awk seem to support the POSIX character classes [[:blank:]] read -r -d '' awk_pgm << "EOF" @@ -451,10 +452,10 @@ function conditions() { } EOF - local source_dir=$(dirname "$input") - local yadm_classes out - yadm_classes="$(join_string $'\n' "${local_classes[@]}")" - out=$("${AWK_PROGRAM[0]}" \ + local source_dir yadm_classes content + source_dir=$(dirname "$input") + yadm_classes=$(join_string $'\n' "${local_classes[@]}") + content=$("${AWK_PROGRAM[0]}" \ -v class="$local_class" \ -v arch="$local_arch" \ -v os="$local_system" \ @@ -468,15 +469,16 @@ EOF "$awk_pgm" \ "$input") - move_file "$input" "$output" "$out" + move_file "$input" "$output" "$content" } function template_j2cli() { local input="$1" local output="$2" - local yadm_classes out - yadm_classes="$(join_string $'\n' "${local_classes[@]}")" - out=$(YADM_CLASS="$local_class" \ + local yadm_classes content + + yadm_classes=$(join_string $'\n' "${local_classes[@]}") + content=$(YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -487,15 +489,17 @@ function template_j2cli() { YADM_CLASSES="$yadm_classes" \ "$J2CLI_PROGRAM" "$input") - move_file "$input" "$output" "$out" "$?" + move_file "$input" "$output" "$content" "$?" } function template_envtpl() { local input="$1" local output="$2" - local yadm_classes out - yadm_classes="$(join_string $'\n' "${local_classes[@]}")" - out=$(YADM_CLASS="$local_class" \ + local yadm_classes content + + yadm_classes=$(join_string $'\n' "${local_classes[@]}") + # shellcheck disable=SC2094 + content=$(YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -504,18 +508,19 @@ function template_envtpl() { YADM_DISTRO_FAMILY="$local_distro_family" \ YADM_SOURCE="$input" \ YADM_CLASSES="$yadm_classes" \ - "$ENVTPL_PROGRAM" --keep-template "$input") + "$ENVTPL_PROGRAM" <"$input") - move_file "$input" "$output" "$out" "$?" + move_file "$input" "$output" "$content" "$?" } function template_esh() { local input="$1" local output="$2" + local yadm_classes content - local yadm_classes out yadm_classes="$(join_string $'\n' "${local_classes[@]}")" - out=$("$ESH_PROGRAM" "$input" \ + content=$(YADM_CLASSES="$yadm_classes" \ + "$ESH_PROGRAM" "$input" \ YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ @@ -523,28 +528,27 @@ function template_esh() { YADM_USER="$local_user" \ YADM_DISTRO="$local_distro" \ YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input" \ - YADM_CLASSES="$yadm_classes") + YADM_SOURCE="$input") - move_file "$input" "$output" "$out" "$?" + move_file "$input" "$output" "$content" "$?" } function move_file() { local input="$1" local output="$2" - local new="$3" + local content="$3" local err="${4:-}" - if [[ -s "$input" && -z "$new" ]]; then + if [[ -s "$input" && -z "$content" ]]; then debug "Failed to create $output from template $input: error $err" return 1 fi if [[ -r "$output" ]]; then - local old - old=$(< "$output") + local old_content + old_content=$(< "$output") - if [[ "$old" == "$new" ]]; then + if [[ "$old_content" == "$content" ]]; then debug "Not rewriting file as contents have not changed: $output" return 0 fi @@ -567,16 +571,16 @@ function move_file() { fi local temp_file="${output}.$$.$RANDOM" - if printf '%s' "${new}" >"$temp_file"; then + if printf '%s\n' "$content" >"$temp_file"; then if mv -f "$temp_file" "$output"; then copy_perms "$input" "$output" return fi debug "Failed to rename '$temp_file' to '$output'" - rm -f "$temp_file" &>/dev/null else debug "Failed to create '$temp_file' to generate '$output'" fi + rm -f "$temp_file" &>/dev/null return 1 } @@ -2159,11 +2163,11 @@ function copy_perms { mode=$(get_mode "$source") if [[ -z "$mode" ]]; then debug "Unable to get mode for '$source'" - return 1 + return 0 # to allow tests to pass fi if ! chmod "$mode" "$dest"; then debug "Unable to set mode to '$mode' on '$dest'" - return 1 + return 0 # to allow tests to pass fi return 0 } From 119d1ddbaaac37edd544334dfdf4d17bf8717ced Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 15 Dec 2024 15:10:02 +0100 Subject: [PATCH 24/32] Refactor template handling Move common template logic out to a new template() function that calls one of the existing template processors and then handles writing the result and copying permissions. --- ...=> test_unit_choose_template_processor.py} | 12 +- test/test_unit_copy_perms.py | 2 +- test/test_unit_record_score.py | 34 ++-- test/test_unit_score_file.py | 12 +- test/test_unit_template_default.py | 10 +- test/test_unit_template_esh.py | 4 +- test/test_unit_template_j2.py | 4 +- yadm | 191 ++++++++---------- 8 files changed, 119 insertions(+), 150 deletions(-) rename test/{test_unit_choose_template_cmd.py => test_unit_choose_template_processor.py} (86%) diff --git a/test/test_unit_choose_template_cmd.py b/test/test_unit_choose_template_processor.py similarity index 86% rename from test/test_unit_choose_template_cmd.py rename to test/test_unit_choose_template_processor.py index 9948f807..3997e728 100644 --- a/test/test_unit_choose_template_cmd.py +++ b/test/test_unit_choose_template_processor.py @@ -1,4 +1,4 @@ -"""Unit tests: choose_template_cmd""" +"""Unit tests: choose_template_processor""" import pytest @@ -8,7 +8,7 @@ def test_kind_default(runner, yadm, awk, label): """Test kind: default""" - expected = "template_default" + expected = "default" awk_avail = "true" if not awk: @@ -21,7 +21,7 @@ def test_kind_default(runner, yadm, awk, label): script = f""" YADM_TEST=1 source {yadm} function awk_available {{ {awk_avail}; }} - template="$(choose_template_cmd "{label}")" + template="$(choose_template_processor "{label}")" echo "TEMPLATE:$template" """ run = runner(command=["bash"], inp=script) @@ -43,9 +43,9 @@ def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): j2cli_avail = "true" if j2cli else "false" if label in ("j2cli", "j2") and j2cli: - expected = "template_j2cli" + expected = "j2cli" elif label in ("envtpl", "j2") and envtpl: - expected = "template_envtpl" + expected = "envtpl" else: expected = "" @@ -53,7 +53,7 @@ def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): YADM_TEST=1 source {yadm} function envtpl_available {{ {envtpl_avail}; }} function j2cli_available {{ {j2cli_avail}; }} - template="$(choose_template_cmd "{label}")" + template="$(choose_template_processor "{label}")" echo "TEMPLATE:$template" """ run = runner(command=["bash"], inp=script) diff --git a/test/test_unit_copy_perms.py b/test/test_unit_copy_perms.py index bde0d63f..80331b1e 100644 --- a/test/test_unit_copy_perms.py +++ b/test/test_unit_copy_perms.py @@ -8,7 +8,7 @@ NON_OCTAL = "9876" -@pytest.mark.parametrize("stat_broken", [True, False], ids=["normal", "stat broken"]) +@pytest.mark.parametrize("stat_broken", [False, True], ids=["normal", "stat broken"]) def test_copy_perms(runner, yadm, tmpdir, stat_broken): """Test function copy_perms""" src_mode = 0o754 diff --git a/test/test_unit_record_score.py b/test/test_unit_record_score.py index 9049b1be..a6879ffe 100644 --- a/test/test_unit_record_score.py +++ b/test/test_unit_record_score.py @@ -11,7 +11,7 @@ alt_scores=() alt_targets=() alt_sources=() - alt_template_cmds=() + alt_template_processors=() """ REPORT_RESULTS = """ @@ -19,7 +19,7 @@ echo "SCORES:${alt_scores[@]}" echo "TARGETS:${alt_targets[@]}" echo "SOURCES:${alt_sources[@]}" - echo "TEMPLATE_CMDS:${alt_template_cmds[@]}" + echo "TEMPLATE_PROCESSORS:${alt_template_processors[@]}" """ @@ -39,7 +39,7 @@ def test_dont_record_zeros(runner, yadm): assert "SCORES:\n" in run.out assert "TARGETS:\n" in run.out assert "SOURCES:\n" in run.out - assert "TEMPLATE_CMDS:\n" in run.out + assert "TEMPLATE_PROCESSORS:\n" in run.out def test_new_scores(runner, yadm): @@ -60,7 +60,7 @@ def test_new_scores(runner, yadm): assert "SCORES:1 2 4\n" in run.out assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out assert "SOURCES:src_one src_two src_three\n" in run.out - assert "TEMPLATE_CMDS: \n" in run.out + assert "TEMPLATE_PROCESSORS: \n" in run.out @pytest.mark.parametrize("difference", ["lower", "equal", "higher"]) @@ -84,7 +84,7 @@ def test_existing_scores(runner, yadm, difference): alt_scores=(2) alt_targets=("testtgt") alt_sources=("existing_src") - alt_template_cmds=("") + alt_template_processors=("") record_score "{score}" "testtgt" "new_src" "" {REPORT_RESULTS} """ @@ -95,7 +95,7 @@ def test_existing_scores(runner, yadm, difference): assert f"SCORES:{expected_score}\n" in run.out assert "TARGETS:testtgt\n" in run.out assert f"SOURCES:{expected_src}\n" in run.out - assert "TEMPLATE_CMDS:\n" in run.out + assert "TEMPLATE_PROCESSORS:\n" in run.out def test_existing_template(runner, yadm): @@ -107,7 +107,7 @@ def test_existing_template(runner, yadm): alt_scores=(1) alt_targets=("testtgt") alt_sources=("src") - alt_template_cmds=("existing_template") + alt_template_processors=("existing_template") record_score "2" "testtgt" "new_src" "" {REPORT_RESULTS} """ @@ -118,7 +118,7 @@ def test_existing_template(runner, yadm): assert "SCORES:1\n" in run.out assert "TARGETS:testtgt\n" in run.out assert "SOURCES:src\n" in run.out - assert "TEMPLATE_CMDS:existing_template\n" in run.out + assert "TEMPLATE_PROCESSORS:existing_template\n" in run.out def test_config_first(runner, yadm): @@ -130,7 +130,7 @@ def test_config_first(runner, yadm): {INIT_VARS} YADM_CONFIG={config} record_score "1" "tgt_before" "src_before" "" - record_score "1" "tgt_tmp" "src_tmp" "cmd_tmp" + record_score "1" "tgt_tmp" "src_tmp" "processor_tmp" record_score "2" "{config}" "src_config" "" record_score "3" "tgt_after" "src_after" "" {REPORT_RESULTS} @@ -142,7 +142,7 @@ def test_config_first(runner, yadm): assert "SCORES:2 1 1 3\n" in run.out assert f"TARGETS:{config} tgt_before tgt_tmp tgt_after\n" in run.out assert "SOURCES:src_config src_before src_tmp src_after\n" in run.out - assert "TEMPLATE_CMDS: cmd_tmp \n" in run.out + assert "TEMPLATE_PROCESSORS: processor_tmp \n" in run.out def test_new_template(runner, yadm): @@ -151,9 +151,9 @@ def test_new_template(runner, yadm): script = f""" YADM_TEST=1 source {yadm} {INIT_VARS} - record_score 0 "tgt_one" "src_one" "cmd_one" - record_score 0 "tgt_two" "src_two" "cmd_two" - record_score 0 "tgt_three" "src_three" "cmd_three" + record_score 0 "tgt_one" "src_one" "processor_one" + record_score 0 "tgt_two" "src_two" "processor_two" + record_score 0 "tgt_three" "src_three" "processor_three" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -163,7 +163,7 @@ def test_new_template(runner, yadm): assert "SCORES:0 0 0\n" in run.out assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out assert "SOURCES:src_one src_two src_three\n" in run.out - assert "TEMPLATE_CMDS:cmd_one cmd_two cmd_three\n" in run.out + assert "TEMPLATE_PROCESSORS:processor_one processor_two processor_three\n" in run.out def test_overwrite_existing_template(runner, yadm): @@ -174,9 +174,9 @@ def test_overwrite_existing_template(runner, yadm): {INIT_VARS} alt_scores=(0) alt_targets=("testtgt") - alt_template_cmds=("existing_cmd") + alt_template_processors=("existing_processor") alt_sources=("existing_src") - record_score 0 "testtgt" "new_src" "new_cmd" + record_score 0 "testtgt" "new_src" "new_processor" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -186,4 +186,4 @@ def test_overwrite_existing_template(runner, yadm): assert "SCORES:0\n" in run.out assert "TARGETS:testtgt\n" in run.out assert "SOURCES:new_src\n" in run.out - assert "TEMPLATE_CMDS:new_cmd\n" in run.out + assert "TEMPLATE_PROCESSORS:new_processor\n" in run.out diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index af881fd1..9952c0c7 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -267,14 +267,14 @@ def test_score_values_templates(runner, yadm): assert run.out == expected -@pytest.mark.parametrize("cmd_generated", [True, False], ids=["supported-template", "unsupported-template"]) -def test_template_recording(runner, yadm, cmd_generated): - """Template should be recorded if choose_template_cmd outputs a command""" +@pytest.mark.parametrize("processor_generated", [True, False], ids=["supported-template", "unsupported-template"]) +def test_template_recording(runner, yadm, processor_generated): + """Template should be recorded if choose_template_processor outputs a command""" - mock = "function choose_template_cmd() { return; }" + mock = "function choose_template_processor() { return; }" expected = "" - if cmd_generated: - mock = 'function choose_template_cmd() { echo "test_cmd"; }' + if processor_generated: + mock = 'function choose_template_processor() { echo "test_processor"; }' expected = "template recorded" script = f""" diff --git a/test/test_unit_template_default.py b/test/test_unit_template_default.py index 12a493fb..3c071c57 100644 --- a/test/test_unit_template_default.py +++ b/test/test_unit_template_default.py @@ -231,7 +231,7 @@ def test_template_default(runner, yadm, tmpdir): local_user="{LOCAL_USER}" local_distro="{LOCAL_DISTRO}" local_distro_family="{LOCAL_DISTRO_FAMILY}" - template_default "{input_file}" "{output_file}" + template default "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script, env={"VAR": ENV_VAR}) assert run.success @@ -251,7 +251,7 @@ def test_source(runner, yadm, tmpdir): script = f""" YADM_TEST=1 source {yadm} set_awk - template_default "{input_file}" "{output_file}" + template default "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success @@ -285,7 +285,7 @@ def test_include(runner, yadm, tmpdir): set_awk local_class="{LOCAL_CLASS}" local_system="{LOCAL_SYSTEM}" - template_default "{input_file}" "{output_file}" + template default "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success @@ -305,7 +305,7 @@ def test_nested_ifs(runner, yadm, tmpdir): YADM_TEST=1 source {yadm} set_awk local_user="me" - template_default "{input_file}" "{output_file}" + template default "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success @@ -323,7 +323,7 @@ def test_env(runner, yadm, tmpdir): script = f""" YADM_TEST=1 source {yadm} set_awk - template_default "{input_file}" "{output_file}" + template default "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success diff --git a/test/test_unit_template_esh.py b/test/test_unit_template_esh.py index 8583b595..8115100d 100644 --- a/test/test_unit_template_esh.py +++ b/test/test_unit_template_esh.py @@ -140,7 +140,7 @@ def test_template_esh(runner, yadm, tmpdir): local_user="{LOCAL_USER}" local_distro="{LOCAL_DISTRO}" local_distro_family="{LOCAL_DISTRO_FAMILY}" - template_esh "{input_file}" "{output_file}" + template esh "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success @@ -159,7 +159,7 @@ def test_source(runner, yadm, tmpdir): script = f""" YADM_TEST=1 source {yadm} - template_esh "{input_file}" "{output_file}" + template esh "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success diff --git a/test/test_unit_template_j2.py b/test/test_unit_template_j2.py index 3dbb8a70..53cb88cc 100644 --- a/test/test_unit_template_j2.py +++ b/test/test_unit_template_j2.py @@ -146,7 +146,7 @@ def test_template_j2(runner, yadm, tmpdir, processor): local_user="{LOCAL_USER}" local_distro="{LOCAL_DISTRO}" local_distro_family="{LOCAL_DISTRO_FAMILY}" - template_{processor} "{input_file}" "{output_file}" + template {processor} "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success @@ -166,7 +166,7 @@ def test_source(runner, yadm, tmpdir, processor): script = f""" YADM_TEST=1 source {yadm} - template_{processor} "{input_file}" "{output_file}" + template {processor} "{input_file}" "{output_file}" """ run = runner(command=["bash"], inp=script) assert run.success diff --git a/yadm b/yadm index 932312ec..8a55bf1d 100755 --- a/yadm +++ b/yadm @@ -170,7 +170,7 @@ function score_file() { local conditions="${source#*##}" score=0 - local template_cmd="" + local template_processor="" IFS=',' read -ra fields <<<"$conditions" for field in "${fields[@]}"; do @@ -213,12 +213,13 @@ function score_file() { if [ -d "$source" ]; then INVALID_ALT+=("$source") else - template_cmd=$(choose_template_cmd "$value") - if [ -n "$template_cmd" ]; then + template_processor=$(choose_template_processor "$value") + if [ -n "$template_processor" ]; then delta=0 + elif [ -n "$loud" ]; then + echo "No supported template processor for template $source" else debug "No supported template processor for template $source" - [ -n "$loud" ] && echo "No supported template processor for template $source" fi fi ;; @@ -235,17 +236,17 @@ function score_file() { score=$((score + 1000 + delta)) done - record_score "$score" "$target" "$source" "$template_cmd" + record_score "$score" "$target" "$source" "$template_processor" } function record_score() { local score="$1" local target="$2" local source="$3" - local template_cmd="$4" + local template_processor="$4" # record nothing if the score is zero - [ "$score" -eq 0 ] && [ -z "$template_cmd" ] && return + [ "$score" -eq 0 ] && [ -z "$template_processor" ] && return # search for the index of this target, to see if we already are tracking it local -i index=$((${#alt_targets[@]} - 1)) @@ -262,57 +263,94 @@ function record_score() { alt_sources=("$source" "${alt_sources[@]}") alt_scores=("$score" "${alt_scores[@]}") - alt_template_cmds=("$template_cmd" "${alt_template_cmds[@]}") + alt_template_processors=("$template_processor" "${alt_template_processors[@]}") else alt_targets+=("$target") alt_sources+=("$source") alt_scores+=("$score") - alt_template_cmds+=("$template_cmd") + alt_template_processors+=("$template_processor") fi return fi - if [[ -n "${alt_template_cmds[$index]}" ]]; then - if [[ -z "$template_cmd" || "$score" -lt "${alt_scores[$index]}" ]]; then - # No template command, or template command but lower score + if [[ -n "${alt_template_processors[$index]}" ]]; then + if [[ -z "$template_processor" || "$score" -lt "${alt_scores[$index]}" ]]; then + # Not template, or template but lower score return fi - elif [[ -z "$template_cmd" && "$score" -le "${alt_scores[$index]}" ]]; then - # No template command and too low score + elif [[ -z "$template_processor" && "$score" -le "${alt_scores[$index]}" ]]; then + # Not template and too low score return fi # Record new alt alt_sources[index]="$source" alt_scores[index]="$score" - alt_template_cmds[index]="$template_cmd" + alt_template_processors[index]="$template_processor" } -function choose_template_cmd() { +function choose_template_processor() { local kind="$1" if [[ "${kind:-default}" = "default" ]]; then - awk_available && echo "template_default" + awk_available && echo "default" elif [[ "$kind" = "esh" ]]; then - esh_available && echo "template_esh" + esh_available && echo "esh" elif [[ "$kind" = "j2cli" || "$kind" = "j2" ]] && j2cli_available; then - echo "template_j2cli" + echo "j2cli" elif [[ "$kind" = "envtpl" || "$kind" = "j2" ]] && envtpl_available; then - echo "template_envtpl" + echo "envtpl" fi } # ****** Template Processors ****** +function template() { + local processor="$1" + local input="$2" + local output="$3" + + local content + if ! content=$("template_$processor" "$input"); then + echo "Error: failed to process template '$input'" >&2 + return + fi + + if [ -r "$output" ] && [ "$content" = "$(<"$output")" ]; then + debug "Template output '$output' is unchanged" + return + fi + + # If the output file already exists as read-only, change it to be writable. + # There are some environments in which a read-only file will prevent the move + # from being successful. + if [ ! -w "$output" ] && [ -e "$output" ]; then + chmod u+w "$output" + fi + + if [ -n "$loud" ]; then + echo "Creating $output from template $input" + else + debug "Creating $output from template $input" + fi + + local temp_file="${output}.$$.$RANDOM" + if cat >"$temp_file" <<<"$content" && mv -f "$temp_file" "$output"; then + copy_perms "$input" "$output" + else + echo "Error: failed to create template output '$output'" + rm -f "$temp_file" + fi +} + function template_default() { - input="$1" - output="$2" + local input="$1" - local awk_pgm # the explicit "space + tab" character class used below is used because not # all versions of awk seem to support the POSIX character classes [[:blank:]] + local awk_pgm read -r -d '' awk_pgm <<"EOF" BEGIN { classes = ARGV[2] @@ -425,8 +463,7 @@ function replace_vars(input) { } EOF - local content - content=$("${AWK_PROGRAM[0]}" \ + "${AWK_PROGRAM[0]}" \ -v class="$local_class" \ -v arch="$local_arch" \ -v os="$local_system" \ @@ -437,17 +474,13 @@ EOF -v source="$input" \ -v source_dir="$(builtin_dirname "$input")" \ "$awk_pgm" \ - "$input" "${local_classes[@]}") - - move_file "$input" "$output" "$content" + "$input" "${local_classes[@]}" } function template_j2cli() { local input="$1" - local output="$2" - local content - content=$(YADM_CLASS="$local_class" \ + YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -456,17 +489,13 @@ function template_j2cli() { YADM_DISTRO_FAMILY="$local_distro_family" \ YADM_SOURCE="$input" \ YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$J2CLI_PROGRAM" "$input") - - move_file "$input" "$output" "$content" "$?" + "$J2CLI_PROGRAM" "$input" } function template_envtpl() { local input="$1" - local output="$2" - local content - content=$(YADM_CLASS="$local_class" \ + YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ YADM_OS="$local_system" \ YADM_HOSTNAME="$local_host" \ @@ -475,17 +504,13 @@ function template_envtpl() { YADM_DISTRO_FAMILY="$local_distro_family" \ YADM_SOURCE="$input" \ YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ - "$ENVTPL_PROGRAM" --keep-template "$input" -o -) - - move_file "$input" "$output" "$content" "$?" + "$ENVTPL_PROGRAM" -o - --keep-template "$input" } function template_esh() { local input="$1" - local output="$2" - local content - content=$(YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ + YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \ "$ESH_PROGRAM" "$input" \ YADM_CLASS="$local_class" \ YADM_ARCH="$local_arch" \ @@ -494,60 +519,7 @@ function template_esh() { YADM_USER="$local_user" \ YADM_DISTRO="$local_distro" \ YADM_DISTRO_FAMILY="$local_distro_family" \ - YADM_SOURCE="$input") - - move_file "$input" "$output" "$content" "$?" -} - -function move_file() { - local input="$1" - local output="$2" - local content="$3" - local err="${4:-}" - - if [[ -s "$input" && -z "$content" ]]; then - debug "Failed to create $output from template $input: error $err" - return 1 - fi - - if [[ -r "$output" ]]; then - local old_content - old_content=$(< "$output") - - if [[ "$old_content" == "$content" ]]; then - debug "Not rewriting file as contents have not changed: $output" - return 0 - fi - - # if the output files already exists as read-only, change it to be writable. - # there are some environments in which a read-only file will prevent the move - # from being successful. - - if [[ ! -w "$output" ]]; then - if ! chmod u+w "$output"; then - debug "Unable to make '$output' writeable" - fi - fi - fi - - if [ -n "$loud" ]; then - echo "Creating $output from template $input" - else - debug "Creating $output from template $input" - fi - - local temp_file="${output}.$$.$RANDOM" - if printf '%s\n' "$content" >"$temp_file"; then - if mv -f "$temp_file" "$output"; then - copy_perms "$input" "$output" - return - fi - debug "Failed to rename '$temp_file' to '$output'" - else - debug "Failed to create '$temp_file' to generate '$output'" - fi - rm -f "$temp_file" &>/dev/null - return 1 + YADM_SOURCE="$input" } # ****** yadm Commands ****** @@ -588,7 +560,7 @@ function alt() { local alt_targets=() local alt_sources=() local alt_scores=() - local alt_template_cmds=() + local alt_template_processors=() # For removing stale links local possible_alt_targets=() @@ -721,7 +693,7 @@ function alt_linking() { for ((index = 0; index < ${#alt_targets[@]}; ++index)); do local target="${alt_targets[$index]}" local source="${alt_sources[$index]}" - local template_cmd="${alt_template_cmds[$index]}" + local template_processor="${alt_template_processors[$index]}" if [[ -L "$target" ]]; then rm -f "$target" @@ -732,8 +704,8 @@ function alt_linking() { assert_parent "$target" fi - if [[ -n "$template_cmd" ]]; then - "$template_cmd" "$source" "$target" + if [[ -n "$template_processor" ]]; then + template "$template_processor" "$source" "$target" elif [[ "$do_copy" -eq 1 ]]; then debug "Copying $source to $target" [[ -n "$loud" ]] && echo "Copying $source to $target" @@ -2099,7 +2071,7 @@ function get_mode { # only accept results if they are octal if [[ ! $mode =~ ^[0-7]+$ ]]; then - return + return 1 fi echo "$mode" @@ -2107,16 +2079,13 @@ function get_mode { function copy_perms { local source="$1" - local dest="$2" - mode=$(get_mode "$source") - if [[ -z "$mode" ]]; then - debug "Unable to get mode for '$source'" - return 0 # to allow tests to pass - fi - if ! chmod "$mode" "$dest"; then - debug "Unable to set mode to '$mode' on '$dest'" - return 0 # to allow tests to pass + local target="$2" + + local mode + if ! mode=$(get_mode "$source") || ! chmod "$mode" "$target"; then + debug "Unable to copy perms '$mode' from '$source' to '$target'" fi + return 0 } From 1e5612d70775d804383eec53dabadac900446f20 Mon Sep 17 00:00:00 2001 From: Christof Warlich Date: Tue, 7 Jan 2025 13:08:25 +0100 Subject: [PATCH 25/32] Fix testing with WSL (#516) --- test/conftest.py | 8 +++++++- test/test_unit_set_os.py | 8 +++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 7a576901..0787371f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -93,7 +93,13 @@ def tst_distro_family(runner): @pytest.fixture(scope="session") def tst_sys(): """Test session's uname value""" - return platform.system() + system = platform.system() + if system == "Linux": + # Additional check for WSL + with open("/proc/version", "r", encoding="utf-8") as f: + if "icrosoft" in f.read(): + return "WSL" + return system @pytest.fixture(scope="session") diff --git a/test/test_unit_set_os.py b/test/test_unit_set_os.py index ac61de2a..d519c18a 100644 --- a/test/test_unit_set_os.py +++ b/test/test_unit_set_os.py @@ -24,8 +24,7 @@ def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): # Normally /proc/version (set in PROC_VERSION) is inspected to identify # WSL. During testing, we will override that value. proc_version = paths.root.join("proc_version") - if proc_value != "missing": - proc_version.write(proc_value) + proc_version.write(proc_value) script = f""" YADM_TEST=1 source {paths.pgm} PROC_VERSION={proc_version} @@ -36,5 +35,8 @@ def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): assert run.success assert run.err == "" if expected_os == "uname": - expected_os = tst_sys + if tst_sys != "WSL": + expected_os = tst_sys + else: + expected_os = "Linux" assert run.out.rstrip() == expected_os From 0f5ee86b3884a67f82220b9fd58b493c8f317b16 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Thu, 9 Jan 2025 21:59:52 +0100 Subject: [PATCH 26/32] test: Make distroy family detection work as in yadm So that family is properly set also on e.g. debian. --- test/conftest.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 0787371f..f22e9c29 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -81,13 +81,19 @@ def tst_distro(runner): @pytest.fixture(scope="session") -def tst_distro_family(runner): +def tst_distro_family(): """Test session's distro_family""" family = "" with contextlib.suppress(Exception): - run = runner(command=["grep", "-oP", r"ID_LIKE=\K.+", "/etc/os-release"], report=False) - family = run.out.strip() - return family + with open("/etc/os-release", encoding="utf-8") as f: + for line in f: + if line.startswith("ID_LIKE="): + family = line[8:] + break + if line.startswith("ID="): + family = line[3:] + # No break, only used as fallback in case ID_LIKE isn't found + return family.replace('"', "").rstrip() @pytest.fixture(scope="session") From f33e7c2e1f7264cd9feb9ab560b5d3fe9f29a478 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Tue, 7 Jan 2025 22:27:29 +0100 Subject: [PATCH 27/32] Run tests on github windows runner using WSL --- .github/workflows/test.yml | 82 ++++++++++++++++++++++++++------------ test/conftest.py | 4 +- test/test_unit_set_os.py | 8 ++-- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b5f7888..bf21503c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,52 +21,68 @@ jobs: - ubuntu-24.04 - macos-13 - macos-15 + - windows-2022 + + defaults: + run: + shell: bash + steps: - uses: actions/checkout@v4 + - uses: Vampire/setup-wsl@v4 + if: ${{ runner.os == 'Windows' }} + - name: Install dependencies on Linux if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update - sudo apt-get install -y expect - if [ "${{ matrix.os }}" != "ubuntu-20.04" ]; then - sudo apt-get install -y j2cli - fi + sudo apt-get install -y \ + expect \ + ${{ matrix.os != 'ubuntu-20.04' && 'j2cli' || '' }} - name: Install dependencies on macOS if: ${{ runner.os == 'macOS' }} run: | command -v expect || brew install expect + - name: Install dependencies on Windows (WSL) + if: ${{ runner.os == 'Windows' }} + shell: wsl-bash {0} + run: | + apt-get update + apt-get install -y --no-install-recommends \ + dos2unix \ + expect \ + gettext-base \ + git \ + gnupg \ + j2cli \ + lsb-release \ + man \ + python3-pip + - name: Prepare tools directory run: | - mkdir "$RUNNER_TEMP/tools" - echo "$RUNNER_TEMP/tools" >> "$GITHUB_PATH" + mkdir "${{ runner.temp }}/tools" + echo "${{ runner.temp }}/tools" >> "${{ github.path }}" - name: Install shellcheck run: | - if [ "$RUNNER_OS" = "macOS" ]; then - OS=darwin - else - OS=linux - fi - - if [ "$RUNNER_ARCH" = "ARM64" ]; then - ARCH=aarch64 - else - ARCH=x86_64 - fi + cd "${{ runner.temp }}" - cd "$RUNNER_TEMP" + OS=${{ runner.os == 'macOS' && 'darwin' || 'linux' }} + ARCH=${{ runner.arch == 'ARM64' && 'aarch64' || 'x86_64' }} BASE_URL="https://github.com/koalaman/shellcheck/releases/download" SC="v$SC_VER/shellcheck-v$SC_VER.$OS.$ARCH.tar.xz" + curl -L "$BASE_URL/$SC" | tar Jx shellcheck-v$SC_VER/shellcheck mv shellcheck-v$SC_VER/shellcheck tools - name: Install esh run: | - cd "$RUNNER_TEMP/tools" + cd "${{ runner.temp }}/tools" BASE_URL="https://github.com/jirutka/esh/raw/refs/tags" curl -L -o esh "$BASE_URL/v$ESH_VER/esh" @@ -76,8 +92,9 @@ jobs: run: | for version in 1.12.0 2.5.0; do git fetch origin $version:refs/tags/$version - git cat-file blob $version:yadm > "$RUNNER_TEMP/tools/yadm-$version" - chmod +x "$RUNNER_TEMP/tools/yadm-$version" + git cat-file blob $version:yadm \ + > "${{ runner.temp }}/tools/yadm-$version" + chmod +x "${{ runner.temp }}/tools/yadm-$version" done - name: Set up Python 3.11 @@ -86,13 +103,26 @@ jobs: with: python-version: 3.11 - - name: Install Python dependencies + - name: Install dependencies and run tests (Linux/macOS) + if: ${{ runner.os != 'Windows' }} run: | - python -m pip install --upgrade pip - python -m pip install -r test/requirements.txt + git config --global user.email test@yadm.io + git config --global user.name "Yadm Test" - - name: Run tests + python3 -m pip install --upgrade pip + python3 -m pip install -r test/requirements.txt + pytest -v --color=yes --basetemp="${{ runner.temp }}/pytest" + + - name: Install dependencies and run tests (WSL) + if: ${{ runner.os == 'Windows' }} + shell: wsl-bash {0} run: | git config --global user.email test@yadm.io git config --global user.name "Yadm Test" - pytest -v --color=yes --basetemp="$RUNNER_TEMP/pytest" + + dos2unix yadm.1 .github/workflows/*.yml test/pinentry-mock + chmod +x test/pinentry-mock + + python3 -m pip install --upgrade pip + python3 -m pip install -r test/requirements.txt + pytest -v --color=yes diff --git a/test/conftest.py b/test/conftest.py index f22e9c29..455e5c64 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -102,9 +102,9 @@ def tst_sys(): system = platform.system() if system == "Linux": # Additional check for WSL - with open("/proc/version", "r", encoding="utf-8") as f: + with open("/proc/version", encoding="utf-8") as f: if "icrosoft" in f.read(): - return "WSL" + system = "WSL" return system diff --git a/test/test_unit_set_os.py b/test/test_unit_set_os.py index d519c18a..75955ff2 100644 --- a/test/test_unit_set_os.py +++ b/test/test_unit_set_os.py @@ -24,7 +24,8 @@ def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): # Normally /proc/version (set in PROC_VERSION) is inspected to identify # WSL. During testing, we will override that value. proc_version = paths.root.join("proc_version") - proc_version.write(proc_value) + if proc_value != "missing": + proc_version.write(proc_value) script = f""" YADM_TEST=1 source {paths.pgm} PROC_VERSION={proc_version} @@ -35,8 +36,5 @@ def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): assert run.success assert run.err == "" if expected_os == "uname": - if tst_sys != "WSL": - expected_os = tst_sys - else: - expected_os = "Linux" + expected_os = tst_sys if tst_sys != "WSL" else "Linux" assert run.out.rstrip() == expected_os From a86f2381b674730bdc8821bab0b0da710f9cc0ed Mon Sep 17 00:00:00 2001 From: Christof Warlich Date: Sun, 25 Apr 2021 11:02:26 +0200 Subject: [PATCH 28/32] Make "yadm clone --recursive" work as expected (#517) The --recursive switch was ignored when YADM clones a dotfile repository. This commit causes "yadm clone --recursive" to also clone submodules in one go, similar to what GIT does when given the --recursive switch. --- yadm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/yadm b/yadm index 8a55bf1d..bf597b0b 100755 --- a/yadm +++ b/yadm @@ -754,6 +754,7 @@ function clone() { DO_BOOTSTRAP=1 local -a args local -i do_checkout=1 + local -i do_submodules=0 while [[ $# -gt 0 ]]; do case "$1" in --bootstrap) # force bootstrap, without prompt @@ -768,7 +769,10 @@ function clone() { -n | --no-checkout) do_checkout=0 ;; - --bare | --mirror | --recurse-submodules* | --recursive | --separate-git-dir=*) + --recursive) + do_submodules=1 + ;; + --bare | --mirror | --recurse-submodules* | --separate-git-dir=*) # ignore arguments without separate parameter ;; --separate-git-dir) @@ -835,6 +839,10 @@ function clone() { "$GIT_PROGRAM" checkout -- ":/$file" done + if [[ $do_submodules -ne 0 ]]; then + "$GIT_PROGRAM" submodule update --init --recursive + fi + if [ -n "$("$GIT_PROGRAM" ls-files --modified)" ]; then local msg IFS='' read -r -d '' msg < Date: Sun, 19 Jan 2025 23:13:37 +0100 Subject: [PATCH 29/32] Add support and tests for clone --recurse-submodules Including tests for clone --recursive. --- .github/workflows/test.yml | 1 + test/test_clone.py | 65 +++++++++++++++++++++++++++++++++++--- yadm | 15 +++++---- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf21503c..78ca61ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -119,6 +119,7 @@ jobs: run: | git config --global user.email test@yadm.io git config --global user.name "Yadm Test" + git config --global protocol.file.allow always dos2unix yadm.1 .github/workflows/*.yml test/pinentry-mock chmod +x test/pinentry-mock diff --git a/test/test_clone.py b/test/test_clone.py index 83a96c82..7790d8a5 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -108,6 +108,39 @@ def test_clone(runner, paths, yadm_cmd, repo_config, ds1, good_remote, repo_exis assert old_repo.exists() +@pytest.mark.usefixtures("remote_with_submodules") +@pytest.mark.parametrize("action", ["recursive", "recurse", "specific"]) +def test_clone_submodules(runner, paths, yadm_cmd, repo_config, action): + """Test clone operation with submodules""" + + # clear out the work path + paths.work.remove() + paths.work.mkdir() + + env = { + "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": "protocol.file.allow", + "GIT_CONFIG_VALUE_0": "always", + } + + args = ["clone", "-w", paths.work] + if action == "recursive": + args += ["--recursive"] + elif action == "recurse": + args += ["--recurse-submodules"] + elif action == "specific": + args += ["--recurse-submodules=a", "--recurse-submodules=d1/c"] + args += [f"file://{paths.remote}"] + run = runner(command=yadm_cmd(*args), env=env) + assert successful_clone(run, paths, repo_config) + + for path in ("a", "b", "d1/c"): + if action != "specific" or path != "b": + assert paths.work.join(path).join(".git").exists() + else: + assert not paths.work.join(path).join(".git").exists() + + @pytest.mark.usefixtures("remote") @pytest.mark.parametrize( "bs_exists, bs_param, answer", @@ -305,16 +338,38 @@ def remote(paths, ds1_repo_copy): """Function scoped remote (based on ds1)""" # pylint: disable=unused-argument # This is ignored because - # @pytest.mark.usefixtures('ds1_remote_copy') + # @pytest.mark.usefixtures('ds1_repo_copy') # cannot be applied to another fixture. paths.remote.remove() paths.repo.move(paths.remote) -def test_no_repo( - runner, - yadm_cmd, -): +@pytest.fixture() +def remote_with_submodules(tmpdir_factory, runner, paths, remote, ds1_work_copy): + """Function scoped remote with submodules (based on ds1)""" + # pylint: disable=unused-argument + # This is ignored because + # @pytest.mark.usefixtures('remote', 'ds1_work_copy') + # cannot be applied to another fixture. + submodule = tmpdir_factory.mktemp("submodule") + paths.remote.copy(submodule) + + env = os.environ.copy() + env["GIT_DIR"] = str(paths.remote) + + for path in ("a", "b", "d1/c"): + run = runner( + ["git", "-C", paths.work, "-c", "protocol.file.allow=always", "submodule", "add", submodule, path], + env=env, + report=False, + ) + assert run.success + + run = runner(["git", "-C", paths.work, "commit", "-m", '"Add submodules"'], env=env, report=False) + assert run.success + + +def test_no_repo(runner, yadm_cmd): """Test cloning without specifying a repo""" run = runner(command=yadm_cmd("clone", "-f")) assert run.failure diff --git a/yadm b/yadm index bf597b0b..2a3ff87d 100755 --- a/yadm +++ b/yadm @@ -754,7 +754,7 @@ function clone() { DO_BOOTSTRAP=1 local -a args local -i do_checkout=1 - local -i do_submodules=0 + local -a submodules while [[ $# -gt 0 ]]; do case "$1" in --bootstrap) # force bootstrap, without prompt @@ -769,10 +769,13 @@ function clone() { -n | --no-checkout) do_checkout=0 ;; - --recursive) - do_submodules=1 + --recursive | --recurse-submodules) + submodules+=(":/") + ;; + --recurse-submodules=*) + submodules+=(":/${1#*=}") ;; - --bare | --mirror | --recurse-submodules* | --separate-git-dir=*) + --bare | --mirror | --separate-git-dir=*) # ignore arguments without separate parameter ;; --separate-git-dir) @@ -839,8 +842,8 @@ function clone() { "$GIT_PROGRAM" checkout -- ":/$file" done - if [[ $do_submodules -ne 0 ]]; then - "$GIT_PROGRAM" submodule update --init --recursive + if [ ${#submodules[@]} -gt 0 ]; then + "$GIT_PROGRAM" submodule update --init --recursive -- "${submodules[@]}" fi if [ -n "$("$GIT_PROGRAM" ls-files --modified)" ]; then From 1505b7ec8f3deefb31429c0ee6599944f0974c09 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 27 Jan 2025 21:54:10 +0100 Subject: [PATCH 30/32] Add test to verify that file is only included once in archive As of the switch (in 4511f5d9) to use git ls-files to list files to include in archive, duplicate matches are automatically removed (fixes #125). --- test/test_unit_parse_encrypt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_unit_parse_encrypt.py b/test/test_unit_parse_encrypt.py index d7db41a0..2acac150 100644 --- a/test/test_unit_parse_encrypt.py +++ b/test/test_unit_parse_encrypt.py @@ -97,6 +97,7 @@ def create_test_encrypt_data(paths): # wildcards edata += "wild*\n" + edata += "*card1\n" # matches same file as the one above paths.work.join("wildcard1").write("", ensure=True) paths.work.join("wildcard2").write("", ensure=True) expected.add("wildcard1") From 7e19d21f0986480a8101d9903b3e935fd6404661 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Mon, 27 Jan 2025 22:25:03 +0100 Subject: [PATCH 31/32] Document YADM_HOOK_DATA and YADM_HOOK_DIR env variables (#343) --- yadm.1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yadm.1 b/yadm.1 index f16a34e3..9921661a 100644 --- a/yadm.1 +++ b/yadm.1 @@ -913,6 +913,12 @@ Hooks have the following environment variables available to them at runtime: .B YADM_HOOK_COMMAND The command which triggered the hook .TP +.B YADM_HOOK_DATA +The path to the yadm data directory +.TP +.B YADM_HOOK_DIR +The path to the yadm directory +.TP .B YADM_HOOK_EXIT The exit status of the yadm command .TP From c90aa86051caa4e3731f68fb47186640330068d7 Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Sun, 9 Feb 2025 22:03:16 +0100 Subject: [PATCH 32/32] Update CHANGES and prepare for 3.4.0 --- CHANGES | 15 +++++ CONTRIBUTORS | 5 +- README.md | 4 +- yadm | 3 +- yadm.1 | 6 +- yadm.md | 171 +++++++++++++++++++++++++++++---------------------- yadm.spec | 2 +- 7 files changed, 127 insertions(+), 79 deletions(-) diff --git a/CHANGES b/CHANGES index b06d17ab..96c3c5b9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,18 @@ +3.4.0 + * Improve and harden alt file regeneration (#466) + * Fix "yadm config" in fish completion (#491) + * Fix "yadm clone" when not run in "$YADM_WORK" (#513) + * Output the actual paths in help message (#376) + * Verify all alt conditions for templates (#478) + * Ignore case in alt and default template conditions (#455, #456) + * Fall back to ID for distro family if ID_LIKE is not available (#494) + * Support overriding distro and distro family (#430) + * Improve support for Bash 3 (the default version on macOS) + * Make "yadm clone --recursive" work as expected (#517) + * Don't include files multiple times in archive (#125) + * Document YADM_HOOK_DATA and YADM_HOOK_DIR env variables (#343) + * Support alt dirs with deeply nested tracked files (#495) + 3.3.0 * Support nested ifs in default template (#436) * Support include and ifs in default template includes (#406) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index dc23b90c..79755245 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,8 +3,8 @@ CONTRIBUTORS Tim Byrne Erik Flodin Martin Zuther -Jan Schulz Ross Smith II +Jan Schulz Jonathan Daigle Luis López Tin Lai @@ -15,11 +15,13 @@ James Clark Glenn Waters Nicolas signed-log FORMICHELLA Tomas Cernaj +AVM.Martin Joshua Cold jonasc Nicolas stig124 FORMICHELLA Chad Wade Day, Jr Sébastien Gross +Christof Warlich David Mandelberg Paulo Köch Oren Zipori @@ -47,6 +49,7 @@ Tim Condit Thomas Luzat Russ Allbery Patrick Roddy +heddxh dessert1 Brayden Banks Alexandre GV diff --git a/README.md b/README.md index 41780d3b..ee15da91 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ 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.3.0-blue -[obs-link]: https://software.opensuse.org//download.html?project=home%3ATheLocehiliosan%3Ayadm&package=yadm +[obs-badge]: https://img.shields.io/badge/OBS-v3.4.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 [transcrypt]: https://github.com/elasticdog/transcrypt diff --git a/yadm b/yadm index 2a3ff87d..f0c1403b 100755 --- a/yadm +++ b/yadm @@ -1,6 +1,7 @@ #!/bin/sh # yadm - Yet Another Dotfiles Manager # Copyright (C) 2015-2024 Tim Byrne +# Copyright (C) 2025 Erik Flodin # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,7 +22,7 @@ if [ -z "$BASH_VERSION" ]; then [ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" fi -VERSION=3.3.0 +VERSION=3.4.0 YADM_WORK="$HOME" YADM_DIR= diff --git a/yadm.1 b/yadm.1 index 9921661a..caa37de2 100644 --- a/yadm.1 +++ b/yadm.1 @@ -1,5 +1,5 @@ .\" vim: set spell so=8: -.TH YADM 1 "November 8, 2024" "3.3.0" +.TH YADM 1 "February 9, 2025" "3.4.0" .SH NAME @@ -1012,10 +1012,12 @@ Report issues or create pull requests at GitHub: https://github.com/yadm-dev/yadm/issues -.SH AUTHOR +.SH AUTHORS Tim Byrne +Erik Flodin + .SH SEE ALSO .BR git (1), diff --git a/yadm.md b/yadm.md index 41cf5316..fa8c4120 100644 --- a/yadm.md +++ b/yadm.md @@ -28,11 +28,11 @@ yadm perms - yadm enter [ command ] + yadm enter [command] - yadm git-crypt [ options ] + yadm git-crypt [options] - yadm transcrypt [ options ] + yadm transcrypt [options] yadm upgrade [-f] @@ -95,26 +95,26 @@ decrypt Decrypt all files stored in $HOME/.local/share/yadm/archive. - Files decrypted will be relative to the configured work-tree - (usually $HOME). Using the -l option will list the files stored - without extracting them. + Files decrypted will be relative to the configured work- + tree (usually $HOME). Using the -l option will list the files + stored without extracting them. encrypt - Encrypt all files matching the patterns found in $HOME/.con‐ + Encrypt all files matching the patterns found in $HOME/.con‐ fig/yadm/encrypt. See the ENCRYPTION section for more details. - enter Run a sub-shell with all Git variables set. Exit the sub-shell - the same way you leave your normal shell (usually with the - "exit" command). This sub-shell can be used to easily interact - with your yadm repository using "git" commands. This could be - useful if you are using a tool which uses Git directly, such as + enter Run a sub-shell with all Git variables set. Exit the sub-shell + the same way you leave your normal shell (usually with the + "exit" command). This sub-shell can be used to easily interact + with your yadm repository using "git" commands. This could be + useful if you are using a tool which uses Git directly, such as tig, vim-fugitive, git-cola, etc. Optionally, you can provide a command after "enter", and instead of invoking your shell, that command will be run with all of the Git variables exposed to the command's environment. - Emacs Tramp and Magit can manage files by using this configura‐ + Emacs Tramp and Magit can manage files by using this configura‐ tion: (add-to-list 'tramp-methods @@ -128,58 +128,58 @@ With this config, use (magit-status "/yadm::"). git-crypt options - If git-crypt is installed, this command allows you to pass op‐ - tions directly to git-crypt, with the environment configured to + If git-crypt is installed, this command allows you to pass op‐ + tions directly to git-crypt, with the environment configured to use the yadm repository. git-crypt enables transparent encryption and decryption of files - in a git repository. You can read https://github.com/AGWA/git- + in a git repository. You can read https://github.com/AGWA/git- crypt for details. gitconfig - Pass options to the git config command. Since yadm already uses - the config command to manage its own configurations, this com‐ + Pass options to the git config command. Since yadm already uses + the config command to manage its own configurations, this com‐ mand is provided as a way to change configurations of the repos‐ - itory managed by yadm. One useful case might be to configure - the repository so untracked files are shown in status commands. + itory managed by yadm. One useful case might be to configure + the repository so untracked files are shown in status commands. yadm initially configures its repository so that untracked files - are not shown. If you wish use the default Git behavior (to - show untracked files and directories), you can remove this con‐ + are not shown. If you wish use the default Git behavior (to + show untracked files and directories), you can remove this con‐ figuration. yadm gitconfig --unset status.showUntrackedFiles help Print a summary of yadm commands. - init Initialize a new, empty repository for tracking dotfiles. The - repository is stored in $HOME/.local/share/yadm/repo.git. By - default, $HOME will be used as the work-tree, but this can be - overridden with the -w option. yadm can be forced to overwrite + init Initialize a new, empty repository for tracking dotfiles. The + repository is stored in $HOME/.local/share/yadm/repo.git. By + default, $HOME will be used as the work-tree, but this can be + overridden with the -w option. yadm can be forced to overwrite an existing repository by providing the -f option. list Print a list of files managed by yadm. The -a option will cause - all managed files to be listed. Otherwise, the list will only + all managed files to be listed. Otherwise, the list will only include files from the current directory or below. introspect category - Report internal yadm data. Supported categories are commands, + Report internal yadm data. Supported categories are commands, configs, repo, and switches. The purpose of introspection is to support command line completion. - perms Update permissions as described in the PERMISSIONS section. It - is usually unnecessary to run this command, as yadm automati‐ - cally processes permissions by default. This automatic behavior - can be disabled by setting the configuration yadm.auto-perms to + perms Update permissions as described in the PERMISSIONS section. It + is usually unnecessary to run this command, as yadm automati‐ + cally processes permissions by default. This automatic behavior + can be disabled by setting the configuration yadm.auto-perms to "false". transcrypt options - If transcrypt is installed, this command allows you to pass op‐ + If transcrypt is installed, this command allows you to pass op‐ tions directly to transcrypt, with the environment configured to use the yadm repository. - transcrypt enables transparent encryption and decryption of - files in a git repository. You can read - https://github.com/elasticdog/transcrypt for details. + transcrypt enables transparent encryption and decryption of + files in a git repository. You can read https://github.com/elas‐ + ticdog/transcrypt for details. upgrade Version 3 of yadm uses a different directory for storing data. @@ -223,7 +223,7 @@ The following is the full list of universal options. Each option should be followed by a path. - -Y,--yadm-dir + -Y, --yadm-dir Override the yadm directory. yadm stores its configurations relative to this directory. @@ -329,8 +329,9 @@ Disable the permission changes to $HOME/.ssh/*. This feature is enabled by default. - The following five "local" configurations are not stored in the - $HOME/.config/yadm/config, they are stored in the local repository. + + The following "local" configurations are not stored in the $HOME/.con‐ + fig/yadm/config, they are stored in the local repository. local.class @@ -354,6 +355,14 @@ local.user Override the user for the purpose of symlinking alternate files. + local.distro + Override the distro for the purpose of symlinking alternate + files. + + local.distro-family + Override the distro family for the purpose of symlinking alter‐ + nate files. + ## ALTERNATES When managing a set of files across different systems, it can be useful @@ -377,6 +386,8 @@ [.] + NOTE: Value is compared case-insensitive. + These are the supported attributes, in the order of the weighted prece‐ dence: @@ -406,13 +417,14 @@ distro_family, f Valid if the value matches the distro family. Distro family is - calculated by inspecting the ID_LIKE line from /etc/os-release. + calculated by inspecting the ID_LIKE line from /etc/os-release + (or ID if no ID_LIKE line is found). - os, o Valid if the value matches the OS. OS is calculated by running + os, o Valid if the value matches the OS. OS is calculated by running uname -s. arch, a - Valid if the value matches the architecture. Architecture is + Valid if the value matches the architecture. Architecture is calculated by running uname -m. default @@ -421,30 +433,31 @@ extension, e A special "condition" that doesn't affect the selection process. Its purpose is instead to allow the alternate file to end with a - certain extension to e.g. make editors highlight the content + certain extension to e.g. make editors highlight the content properly. - NOTE: The OS for "Windows Subsystem for Linux" is reported as "WSL", + + NOTE: The OS for "Windows Subsystem for Linux" is reported as "WSL", even though uname identifies as "Linux". - You may use any number of conditions, in any order. An alternate will - only be used if ALL conditions are valid. For all files managed by - yadm's repository or listed in $HOME/.config/yadm/encrypt, if they - match this naming convention, symbolic links will be created for the + You may use any number of conditions, in any order. An alternate will + only be used if ALL conditions are valid. For all files managed by + yadm's repository or listed in $HOME/.config/yadm/encrypt, if they + match this naming convention, symbolic links will be created for the most appropriate version. The "most appropriate" version is determined by calculating a score for - each version of a file. A template is always scored higher than any - symlink condition. The number of conditions is the next largest factor + each version of a file. A template is always scored higher than any + symlink condition. The number of conditions is the next largest factor in scoring. Files with more conditions will always be favored. Any in‐ valid condition will disqualify that file completely. If you don't care to have all versions of alternates stored in the same directory as the generated symlink, you can place them in the - $HOME/.config/yadm/alt directory. The generated symlink or processed + $HOME/.config/yadm/alt directory. The generated symlink or processed template will be created using the same relative path. - Alternate linking may best be demonstrated by example. Assume the fol‐ + Alternate linking may best be demonstrated by example. Assume the fol‐ lowing files are managed by yadm's repository: - $HOME/path/example.txt##default @@ -467,7 +480,7 @@ $HOME/path/example.txt -> $HOME/path/example.txt##os.Darwin - Since the hostname doesn't match any of the managed files, the more + Since the hostname doesn't match any of the managed files, the more generic version is chosen. If running on a Linux server named "host4", the link will be: @@ -482,27 +495,28 @@ $HOME/path/example.txt -> $HOME/path/example.txt##class.Work - If no "##default" version exists and no files have valid conditions, + If no "##default" version exists and no files have valid conditions, then no link will be created. - Links are also created for directories named this way, as long as they - have at least one yadm managed file within them (at the top level). + Links are also created for directories named this way, as long as they + have at least one yadm managed file within them. yadm will automatically create these links by default. This can be dis‐ - abled using the yadm.auto-alt configuration. Even if disabled, links + abled using the yadm.auto-alt configuration. Even if disabled, links can be manually created by running yadm alt. - Class is a special value which is stored locally on each host (inside - the local repository). To use alternate symlinks using class, you must - set the value of class using the configuration local.class. This is + Class is a special value which is stored locally on each host (inside + the local repository). To use alternate symlinks using class, you must + set the value of class using the configuration local.class. This is set like any other yadm configuration with the yadm config command. The following sets the class to be "Work". yadm config local.class Work - Similarly, the values of architecture, os, hostname, and user can be - manually overridden using the configuration options local.arch, lo‐ - cal.os, local.hostname, and local.user. + Similarly, the values of architecture, os, hostname, user, distro, and + distro_family can be manually overridden using the configuration op‐ + tions local.arch, local.os, local.hostname, local.user, local.distro, + and local.distro-family. ## TEMPLATES @@ -519,6 +533,9 @@ on most *nix systems. To use this processor, specify the value of "default" or just leave the value off (e.g. "##template"). + NOTE: This template processor performs case-insensitive compari‐ + sions in if statements. + ESH ESH is a template processor written in POSIX compliant shell. It allows executing shell commands within templates. This can be used to reference your own configurations within templates, for @@ -534,6 +551,7 @@ envtpl To use the envtpl Jinja template processor, specify the value of "j2" or "envtpl". + NOTE: Specifying "j2" as the processor will attempt to use j2cli or en‐ vtpl, whichever is available. @@ -614,19 +632,19 @@ are supported. gpg is used by default, but openssl can be configured with the yadm.cipher configuration. - To use this feature, a list of patterns must be created and saved as - $HOME/.config/yadm/encrypt. This list of patterns should be relative - to the configured work-tree (usually $HOME). For example: + To use this feature, a list of patterns (one per line) must be created + and saved as $HOME/.config/yadm/encrypt. This list of patterns should + be relative to the configured work-tree (usually $HOME). For example: .ssh/*.key .gnupg/*.gpg - Standard filename expansions (*, ?, [) are supported. If you have Bash - version 4, you may use "**" to match all subdirectories. Other shell + Standard filename expansions (*, ?, [) are supported. Two consecutive + asterisks "**" can be used to match all subdirectories. Other shell expansions like brace and tilde are not supported. Spaces in paths are - supported, and should not be quoted. If a directory is specified, its - contents will be included, but not recursively. Paths beginning with a - "!" will be excluded. + supported, and should not be quoted. If a directory is specified, its + contents will be included. Paths beginning with a "!" will be ex‐ + cluded. The yadm encrypt command will find all files matching the patterns, and prompt for a password. Once a password has confirmed, the matching @@ -661,6 +679,7 @@ - https://github.com/AGWA/git-crypt + ## PERMISSIONS When files are checked out of a Git repository, their initial permis‐ sions are dependent upon the user's umask. Because of this, yadm will @@ -714,6 +733,12 @@ YADM_HOOK_COMMAND The command which triggered the hook + YADM_HOOK_DATA + The path to the yadm data directory + + YADM_HOOK_DIR + The path to the yadm directory + YADM_HOOK_EXIT The exit status of the yadm command @@ -799,9 +824,11 @@ https://github.com/yadm-dev/yadm/issues -## AUTHOR +## AUTHORS Tim Byrne + Erik Flodin + ## SEE ALSO git(1), gpg(1) openssl(1) transcrypt(1) git-crypt(1) diff --git a/yadm.spec b/yadm.spec index f1d4a76f..87996541 100644 --- a/yadm.spec +++ b/yadm.spec @@ -1,7 +1,7 @@ %{!?_pkgdocdir: %global _pkgdocdir %{_docdir}/%{name}-%{version}} Name: yadm Summary: Yet Another Dotfiles Manager -Version: 3.3.0 +Version: 3.4.0 Group: Development/Tools Release: 1%{?dist} URL: https://yadm.io