diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1dae7cff..78ca61ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,13 +1,129 @@ --- 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 + - windows-2022 + + defaults: + run: + shell: bash + steps: - - uses: actions/checkout@v2 - - name: Tests - run: make test + - 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 \ + ${{ 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 }}" + + - name: Install shellcheck + run: | + 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" + + 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 dependencies and run tests (Linux/macOS) + if: ${{ runner.os != 'Windows' }} + run: | + git config --global user.email test@yadm.io + git config --global user.name "Yadm Test" + + 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" + git config --global protocol.file.allow always + + 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/.gitignore b/.gitignore index 53249476..7f43fdb6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .testyadm _site testenv +__pycache__/ 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/Makefile b/Makefile index 5da09185..db485125 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 @@ -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/README.md b/README.md index 4f031267..abc73357 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,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/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 +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..455e5c64 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") @@ -82,19 +81,31 @@ 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") 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", encoding="utf-8") as f: + if "icrosoft" in f.read(): + system = "WSL" + return system @pytest.fixture(scope="session") @@ -140,6 +151,8 @@ def supported_configs(): return [ "local.arch", "local.class", + "local.distro", + "local.distro-family", "local.hostname", "local.os", "local.user", @@ -246,7 +259,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 +588,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 +611,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..ef421f77 100644 --- a/test/test_alt.py +++ b/test/test_alt.py @@ -1,4 +1,5 @@ """Test alt""" + import os import string @@ -169,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_clone.py b/test/test_clone.py index 1cae9297..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 @@ -326,3 +381,31 @@ 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, 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) + + # 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 == "" + assert run.err == "" 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_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/test/test_unit_choose_template_cmd.py b/test/test_unit_choose_template_processor.py similarity index 77% rename from test/test_unit_choose_template_cmd.py rename to test/test_unit_choose_template_processor.py index cf7600b1..3997e728 100644 --- a/test/test_unit_choose_template_cmd.py +++ b/test/test_unit_choose_template_processor.py @@ -1,4 +1,5 @@ -"""Unit tests: choose_template_cmd""" +"""Unit tests: choose_template_processor""" + import pytest @@ -7,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: @@ -19,8 +20,8 @@ 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}")" + function awk_available {{ {awk_avail}; }} + template="$(choose_template_processor "{label}")" echo "TEMPLATE:$template" """ run = runner(command=["bash"], inp=script) @@ -42,17 +43,17 @@ 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 = "" script = f""" YADM_TEST=1 source {yadm} - function envtpl_available {{ { envtpl_avail}; }} - function j2cli_available {{ { j2cli_avail}; }} - template="$(choose_template_cmd "{label}")" + function envtpl_available {{ {envtpl_avail}; }} + function j2cli_available {{ {j2cli_avail}; }} + template="$(choose_template_processor "{label}")" echo "TEMPLATE:$template" """ run = runner(command=["bash"], inp=script) 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 diff --git a/test/test_unit_copy_perms.py b/test/test_unit_copy_perms.py index c0ea04fe..80331b1e 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 @@ -7,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_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_parse_encrypt.py b/test/test_unit_parse_encrypt.py index 6a5c23bf..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") @@ -105,7 +106,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 +188,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/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..52fbb315 100644 --- a/test/test_unit_query_distro_family.py +++ b/test/test_unit_query_distro_family.py @@ -1,15 +1,18 @@ """Unit tests: query_distro_family""" + 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}" @@ -18,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/test/test_unit_record_score.py b/test/test_unit_record_score.py index a82046cf..a6879ffe 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 = """ @@ -10,7 +11,7 @@ alt_scores=() alt_targets=() alt_sources=() - alt_template_cmds=() + alt_template_processors=() """ REPORT_RESULTS = """ @@ -18,6 +19,7 @@ echo "SCORES:${alt_scores[@]}" echo "TARGETS:${alt_targets[@]}" echo "SOURCES:${alt_sources[@]}" + echo "TEMPLATE_PROCESSORS:${alt_template_processors[@]}" """ @@ -37,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_PROCESSORS:\n" in run.out def test_new_scores(runner, yadm): @@ -45,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) @@ -57,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_PROCESSORS: \n" in run.out @pytest.mark.parametrize("difference", ["lower", "equal", "higher"]) @@ -80,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_processors=("") + record_score "{score}" "testtgt" "new_src" "" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -90,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_PROCESSORS:\n" in run.out def test_existing_template(runner, yadm): @@ -100,9 +106,9 @@ def test_existing_template(runner, yadm): {INIT_VARS} alt_scores=(1) alt_targets=("testtgt") - alt_sources=() - alt_template_cmds=("existing_template") - record_score "2" "testtgt" "new_src" + alt_sources=("src") + alt_template_processors=("existing_template") + record_score "2" "testtgt" "new_src" "" {REPORT_RESULTS} """ run = runner(command=["bash"], inp=script) @@ -111,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_PROCESSORS:existing_template\n" in run.out def test_config_first(runner, yadm): @@ -122,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" "processor_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_PROCESSORS: processor_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" "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) + 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_PROCESSORS:processor_one processor_two processor_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_processors=("existing_processor") + alt_sources=("existing_src") + record_score 0 "testtgt" "new_src" "new_processor" + {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_PROCESSORS:new_processor\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_relative_path.py b/test/test_unit_relative_path.py index e0b32f50..0d3075fb 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 @@ -10,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/test/test_unit_remove_stale_links.py b/test/test_unit_remove_stale_links.py index f389ed80..275832d2 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 @@ -24,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_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..9952c0c7 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 = { @@ -53,42 +54,42 @@ 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 break - elif label in TEMPLATE_LABELS: + elif label not in TEMPLATE_LABELS: score = 0 break return score @@ -104,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: @@ -189,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" """ @@ -254,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" """ @@ -266,19 +267,19 @@ 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""" 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" """ @@ -288,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/test_unit_set_local_alt_values.py b/test/test_unit_set_local_alt_values.py index fa5749d3..f7d3877f 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 @@ -12,6 +13,8 @@ "os", "hostname", "user", + "distro", + "distro-family", ], ids=[ "no-override", @@ -20,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 && @@ -33,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": @@ -47,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 - + default_values = { + "class": "", + "arch": tst_arch, + "os": tst_sys, + "hostname": tst_host, + "user": tst_user, + "distro": tst_distro, + "distro-family": tst_distro_family, + } -def test_distro_and_family(runner, yadm): - """Assert that local_distro/local_distro_family are set""" - - 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/test/test_unit_set_os.py b/test/test_unit_set_os.py index ac61de2a..75955ff2 100644 --- a/test/test_unit_set_os.py +++ b/test/test_unit_set_os.py @@ -36,5 +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": - expected_os = tst_sys + expected_os = tst_sys if tst_sys != "WSL" else "Linux" assert run.out.rstrip() == expected_os 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_default.py b/test/test_unit_template_default.py index aaae0fec..3c071c57 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}" %}} +{{% if yadm.distro == "{LOCAL_DISTRO.lower()}" %}} Included section for distro = {{{{yadm.distro}}}} ({{{{yadm.distro}}}} again) {{% endif %}} {{% if yadm.distro == "wrongdistro2" %}} @@ -95,14 +95,14 @@ {{% 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 %}} {{% 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" %}} @@ -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 2c91c206..8115100d 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 @@ -139,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 @@ -158,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 750ee8c6..53cb88cc 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 @@ -145,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 @@ -165,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/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 }} 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: 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 547121b4..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= @@ -89,21 +90,21 @@ function main() { param="${param//\\/\\\\}" param="${param//$_tab/\\$_tab}" param="${param// /\\ }" - _fc+=( "$param" ) + _fc+=("$param") done FULL_COMMAND="${_fc[*]}" # create the YADM_DIR & YADM_DATA if they doesn't exist yet - [ -d "$YADM_DIR" ] || mkdir -p "$YADM_DIR" + [ -d "$YADM_DIR" ] || mkdir -p "$YADM_DIR" [ -d "$YADM_DATA" ] || mkdir -p "$YADM_DATA" # parse command line arguments local retval=0 internal_commands="^(alt|bootstrap|clean|clone|config|decrypt|encrypt|enter|git-crypt|help|--help|init|introspect|list|perms|transcrypt|upgrade|version|--version)$" - if [ -z "$*" ] ; then + if [ -z "$*" ]; then # no argumnts will result in help() help - elif [[ "$1" =~ $internal_commands ]] ; then + elif [[ "$1" =~ $internal_commands ]]; then # for internal commands, process all of the arguments YADM_COMMAND="${1//-/_}" YADM_COMMAND="${YADM_COMMAND/__/}" @@ -111,32 +112,32 @@ function main() { shift # commands listed below do not process any of the parameters - if [[ "$YADM_COMMAND" =~ ^(enter|git_crypt)$ ]] ; then + if [[ "$YADM_COMMAND" =~ ^(enter|git_crypt)$ ]]; then YADM_ARGS=("$@") else - while [[ $# -gt 0 ]] ; do + while [[ $# -gt 0 ]]; do key="$1" case $key in -a) # used by list() LIST_ALL="YES" - ;; + ;; -d) # used by all commands DEBUG="YES" - ;; + ;; -f) # used by init(), clone() and upgrade() FORCE="YES" - ;; + ;; -l) # used by decrypt() DO_LIST="YES" [[ "$YADM_COMMAND" =~ ^(clone|config)$ ]] && YADM_ARGS+=("$1") - ;; + ;; -w) # used by init() and clone() YADM_WORK="$(qualify_path "$2" "work tree")" shift - ;; + ;; *) # any unhandled arguments YADM_ARGS+=("$1") - ;; + ;; esac shift done @@ -162,212 +163,196 @@ 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 - IFS=',' read -ra fields <<< "$conditions" + local template_processor="" + + 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 + + shopt -s nocasematch + 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) + [[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 + ;; + f | distro_family) + [[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 + ;; + 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_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" + fi + fi + ;; + *) + INVALID_ALT+=("$source") + ;; + esac + shopt -u nocasematch + + if ((delta < 0)); then score=0 return fi + score=$((score + 1000 + delta)) done - record_score "$score" "$tgt" "$src" + record_score "$score" "$target" "$source" "$template_processor" } function record_score() { - score="$1" - tgt="$2" - src="$3" + local score="$1" + local target="$2" + local source="$3" + local template_processor="$4" # record nothing if the score is zero - [ "$score" -eq 0 ] && return + [ "$score" -eq 0 ] && [ -z "$template_processor" ] && 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 + if [ "$target" = "$YADM_CONFIG" ]; then + alt_targets=("$target" "${alt_targets[@]}") + + alt_sources=("$source" "${alt_sources[@]}") + alt_scores=("$score" "${alt_scores[@]}") + alt_template_processors=("$template_processor" "${alt_template_processors[@]}") 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 + alt_targets+=("$target") - # record nothing if a template command is registered for this file - [ "${alt_template_cmds[$index]+isset}" ] && return + alt_sources+=("$source") + alt_scores+=("$score") + alt_template_processors+=("$template_processor") + fi + return + fi - # record higher scoring sources - if [ "$score" -gt "${alt_scores[$index]}" ]; then - alt_scores[index]="$score" - alt_sources[index]="$src" + 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_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_processors[index]="$template_processor" } -function record_template() { - tgt="$1" - cmd="$2" - src="$3" +function choose_template_processor() { + local kind="$1" - # 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 - 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 + if [[ "${kind:-default}" = "default" ]]; then + awk_available && echo "default" + elif [[ "$kind" = "esh" ]]; then + esh_available && echo "esh" + elif [[ "$kind" = "j2cli" || "$kind" = "j2" ]] && j2cli_available; then + echo "j2cli" + elif [[ "$kind" = "envtpl" || "$kind" = "j2" ]] && envtpl_available; then + echo "envtpl" fi - # record the template command, last one wins - alt_template_cmds[index]="$cmd" - alt_sources[index]="$src" - } -function choose_template_cmd() { - kind="$1" +# ****** 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 [ "$kind" = "default" ] || [ "$kind" = "" ] && awk_available; then - echo "template_default" - elif [ "$kind" = "esh" ] && esh_available; then - echo "template_esh" - elif [ "$kind" = "j2cli" ] || [ "$kind" = "j2" ] && j2cli_available; then - echo "template_j2cli" - elif [ "$kind" = "envtpl" ] || [ "$kind" = "j2" ] && envtpl_available; then - echo "template_envtpl" + # 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 - return # this "kind" of template is not supported + 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 } -# ****** Template Processors ****** - function template_default() { - input="$1" - output="$2" - temp_file="${output}.$$.$RANDOM" + local input="$1" # 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" + local awk_pgm + read -r -d '' awk_pgm <<"EOF" BEGIN { classes = ARGV[2] for (i = 3; i < ARGC; ++i) { @@ -403,17 +388,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 { - lhs = replace_vars("{{" lhs "}}") + lhs = tolower(replace_vars("{{" lhs "}}")) } if (op == "==") { skip[++level] = lhs != rhs } @@ -488,84 +473,54 @@ 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" - - move_file "$input" "$output" "$temp_file" + "$input" "${local_classes[@]}" } function template_j2cli() { - input="$1" - 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" + local input="$1" - move_file "$input" "$output" "$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" } function template_envtpl() { - input="$1" - 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" + local input="$1" - move_file "$input" "$output" "$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" -o - --keep-template "$input" } function template_esh() { - input="$1" - output="$2" - temp_file="${output}.$$.$RANDOM" + local input="$1" 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" - - move_file "$input" "$output" "$temp_file" -} - -function move_file() { - local input=$1 - local output=$2 - local temp_file=$3 - - [ ! -f "$temp_file" ] && return - - # 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" - - mv -f "$temp_file" "$output" - copy_perms "$input" "$output" + "$ESH_PROGRAM" "$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" } # ****** yadm Commands ****** @@ -599,29 +554,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_processors=() + + # 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() { @@ -654,7 +625,7 @@ function report_invalid_alts() { ${path_list} *********** EOF - printf '%s\n' "$msg" >&2 + printf '%s\n' "$msg" >&2 } function remove_stale_links() { @@ -662,7 +633,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,94 +652,83 @@ 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" - done <<< "$all_classes" + local_classes+=("$class") + local_class="$class" + 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 } 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_processor="${alt_template_processors[$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_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" - 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 + cp -f "$source" "$target" + else + debug "Linking $source to $target" + [[ -n "$loud" ]] && echo "Linking $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 + ln_relative "$source" "$target" fi done - } 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 -fs "$rel_source" "$target" alt_linked+=("$rel_source") } @@ -795,30 +755,37 @@ function clone() { DO_BOOTSTRAP=1 local -a args local -i do_checkout=1 - while [[ $# -gt 0 ]] ; do + local -a submodules + 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=*) + ;; + --recursive | --recurse-submodules) + submodules+=(":/") + ;; + --recurse-submodules=*) + submodules+=(":/${1#*=}") + ;; + --bare | --mirror | --separate-git-dir=*) # ignore arguments without separate parameter - ;; + ;; --separate-git-dir) # ignore arguments with separate parameter shift - ;; + ;; *) args+=("$1") - ;; + ;; esac shift done @@ -843,17 +810,17 @@ 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" # then reset the index as the --no-checkout flag makes the index empty - "$GIT_PROGRAM" reset --quiet -- . + "$GIT_PROGRAM" reset --quiet -- ":/" if [ "$YADM_WORK" = "$HOME" ]; then debug "Determining if repo tracks private directories" @@ -868,17 +835,21 @@ 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,11 +1179,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 @@ -1239,9 +1217,9 @@ function init() { function introspect() { case "$1" in - commands|configs|repo|switches) + commands | configs | repo | switches) "introspect_$1" - ;; + ;; esac } @@ -1275,6 +1253,8 @@ function introspect_configs() { read -r -d '' msg <<-EOF local.arch local.class +local.distro +local.distro-family local.hostname local.os local.user @@ -1320,7 +1300,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 @@ -1345,13 +1325,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 @@ -1362,7 +1342,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 } @@ -1373,7 +1353,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 @@ -1400,18 +1380,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" @@ -1431,7 +1411,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" @@ -1439,13 +1419,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" @@ -1453,7 +1431,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" @@ -1463,13 +1441,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 @@ -1479,7 +1457,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 @@ -1508,7 +1487,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="" @@ -1522,13 +1501,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 @@ -1536,73 +1515,75 @@ function exclude_encrypted() { } function query_distro() { - distro="" - if command -v "$LSB_RELEASE_PROGRAM" &> /dev/null; then + local distro="" + 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" } 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" + 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 @@ -1610,17 +1591,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() { @@ -1628,7 +1609,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" @@ -1637,7 +1618,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" @@ -1661,17 +1642,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 @@ -1708,7 +1687,7 @@ ${path_list} *********** EOF printf '%s\n' "$msg" >&2 -LEGACY_WARNING_ISSUED=1 + LEGACY_WARNING_ISSUED=1 } @@ -1790,15 +1769,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 } @@ -1806,7 +1784,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 } @@ -1836,8 +1814,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 @@ -1902,7 +1880,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 } @@ -1940,125 +1918,93 @@ 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 - - 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 + local -a exclude + local -a include + + while IFS= read -r pattern; do + case $pattern in + \#*) + # Ignore comments + ;; + !*) + exclude+=("--exclude=${pattern:1}") + ;; + *) + if ! [[ $pattern =~ ^[[:blank:]]*$ ]]; then + include+=("$pattern") fi - done - fi - 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 + ;; + esac + done <"$YADM_ENCRYPT" - 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() { # 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 ****** @@ -2066,9 +2012,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 @@ -2078,9 +2024,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 @@ -2094,12 +2040,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 - mode="" + if [[ ! $mode =~ ^[0-7]+$ ]]; then + return 1 fi echo "$mode" @@ -2145,9 +2091,13 @@ function get_mode { function copy_perms { local source="$1" - local dest="$2" - mode=$(get_mode "$source") - [ -n "$mode" ] && chmod "$mode" "$dest" + 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 } @@ -2171,11 +2121,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() { @@ -2184,11 +2134,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() { @@ -2197,11 +2147,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() { @@ -2211,11 +2161,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() { @@ -2223,23 +2173,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 } @@ -2247,7 +2197,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" @@ -2255,7 +2205,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" @@ -2279,7 +2229,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 diff --git a/yadm.1 b/yadm.1 index 5dd08fd7..caa37de2 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 "February 9, 2025" "3.4.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,8 +425,8 @@ Disable the permission changes to .IR $HOME/.ssh/* . This feature is enabled by default. -.RE -The following five "local" configurations are not stored in the +.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 @@ -452,6 +451,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 @@ -474,25 +479,28 @@ 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 -.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. @@ -501,37 +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 -.BR "/etc/os-release" . +.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". @@ -577,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: @@ -595,7 +605,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 @@ -614,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 @@ -637,6 +649,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 @@ -652,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. @@ -666,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 : @@ -749,11 +765,11 @@ 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 -.IR $HOME ). +.IR work-tree \ (usually\ $HOME ). For example: .RS @@ -761,20 +777,20 @@ 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 .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 @@ -817,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 @@ -826,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 @@ -898,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 @@ -976,7 +997,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 @@ -991,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