1
0
mirror of https://github.com/TheLocehiliosan/yadm synced 2025-06-02 15:43:59 +00:00

Release 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)
This commit is contained in:
Erik Flodin 2025-02-09 22:24:49 +01:00
commit 5648f8b337
No known key found for this signature in database
GPG Key ID: 420A7C865EE3F85F
48 changed files with 1275 additions and 1010 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
.testyadm
_site
testenv
__pycache__/

15
CHANGES
View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 </dev/tty
case $choice in
1)
REPO_URL="https://github.com/"
@ -97,7 +99,7 @@ function build_url() {
*)
echo
echo Please specify the full URL of your dotfiles repo
read -r -p "URL ->" choice < /dev/tty
read -r -p "URL ->" choice </dev/tty
REPO_URL="$choice"
return
;;
@ -107,7 +109,7 @@ function build_url() {
echo "Provide your user and repo separated by '/'"
echo "For example: UserName/dotfiles"
echo
read -r -p "User/Repo ->" choice < /dev/tty
read -r -p "User/Repo ->" choice </dev/tty
[[ "$choice" =~ ^[^[:space:]]+/[^[:space:]]+$ ]] || {
echo "Not formatted as USER/REPO"
REPO_URL=

View File

@ -60,7 +60,7 @@ complete -x -c yadm -n '__fish_yadm_using_command introspect' -a (printf -- '%s\
complete -x -c yadm -n '__fish_yadm_needs_command' -a 'gitconfig' -d 'Pass options to the git config command'
complete -x -c yadm -n '__fish_yadm_needs_command' -a 'config' -d 'Configure a setting'
for name in (yadm introspect configs)
complete -x -c yadm -n '__fish_yadm_using_command config' -a '$name' -d 'yadm config'
complete -x -c yadm -n '__fish_yadm_using_command config' -a $name -d 'yadm config'
end
# yadm universial options

View File

@ -7,6 +7,7 @@ markers = [
[tool.pylint.design]
max-args = 14
max-positional-arguments = 10
max-locals = 28
max-attributes = 8
max-statements = 65

View File

@ -1,8 +1,7 @@
FROM ubuntu:23.04
MAINTAINER Tim Byrne <sultan@locehilios.com>
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

View File

@ -9,7 +9,6 @@ import pwd
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 @@ class Runner:
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 gnupg(tmpdir_factory, runner):
# 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)

View File

@ -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";

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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 == ""

View File

@ -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)

View File

@ -1,4 +1,5 @@
"""Test help"""
import pytest

View File

@ -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():

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: choose_template_cmd"""
"""Unit tests: choose_template_processor"""
import pytest
@ -7,7 +8,7 @@ import pytest
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)

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: copy_perms"""
import os
import pytest
@ -7,7 +8,7 @@ OCTAL = "7654"
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

View File

@ -1,4 +1,5 @@
"""Unit tests: exclude_encrypted"""
import pytest

View File

@ -1,4 +1,5 @@
"""Unit tests: issue_legacy_path_warning"""
import pytest

View File

@ -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"

View File

@ -1,4 +1,5 @@
"""Unit tests: private_dirs"""
import pytest

View File

@ -1,4 +1,5 @@
"""Unit tests: query_distro"""
import pytest

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: record_score"""
import pytest
INIT_VARS = """
@ -10,7 +11,7 @@ INIT_VARS = """
alt_scores=()
alt_targets=()
alt_sources=()
alt_template_cmds=()
alt_template_processors=()
"""
REPORT_RESULTS = """
@ -18,6 +19,7 @@ REPORT_RESULTS = """
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}
"""
run = runner(command=["bash"], inp=script)
assert run.success
assert run.err == ""
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 "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}
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 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 "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

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: relative_path"""
import pytest
@ -10,11 +11,16 @@ import pytest
("/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):

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: report_invalid_alts"""
import pytest

View File

@ -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,
}

View File

@ -1,4 +1,5 @@
"""Unit tests: set_local_alt_values"""
import pytest
import utils
@ -12,6 +13,8 @@ import utils
"os",
"hostname",
"user",
"distro",
"distro-family",
],
ids=[
"no-override",
@ -20,11 +23,15 @@ import utils
"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
default_values = {
"class": "",
"arch": tst_arch,
"os": tst_sys,
"hostname": tst_host,
"user": tst_user,
"distro": tst_distro,
"distro-family": tst_distro_family,
}
if override == "arch":
assert "arch='override'" in run.out
else:
assert f"arch='{tst_arch}'" in run.out
if override == "os":
assert "os='override'" in run.out
else:
assert f"os='{tst_sys}'" in run.out
if override == "hostname":
assert "host='override'" in run.out
else:
assert f"host='{tst_host}'" in run.out
if override == "user":
assert "user='override'" in run.out
else:
assert f"user='{tst_user}'" in run.out
def test_distro_and_family(runner, yadm):
"""Assert that local_distro/local_distro_family are set"""
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

View File

@ -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

View File

@ -1,4 +1,5 @@
"""Unit tests: set_yadm_dirs"""
import pytest

View File

@ -35,13 +35,13 @@ wrong class 1
{{% 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 @@ wrong class 2
{{% 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 @@ wrong arch 2
{{% 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 @@ wrong os 2
{{% 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 @@ wrong host 2
{{% 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 @@ wrong user 2
{{% 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 @@ wrong distro 2
{{% 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

View File

@ -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

View File

@ -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

View File

@ -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
}}

View File

@ -19,13 +19,15 @@ import pytest
],
)
@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:

View File

@ -12,7 +12,7 @@ ALT_DIR = "test alt/test alt dir"
# 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)

1094
yadm

File diff suppressed because it is too large Load Diff

243
yadm.1
View File

@ -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 <additional-class>
yadm config \-\-add local.class <additional-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.
<attribute>[.<value>]
.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 <url>
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 <sultan@locehilios.com>
Erik Flodin <erik@flodin.me>
.SH SEE ALSO
.BR git (1),

171
yadm.md
View File

@ -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 @@
<attribute>[.<value>]
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 <sultan@locehilios.com>
Erik Flodin <erik@flodin.me>
## SEE ALSO
git(1), gpg(1) openssl(1) transcrypt(1) git-crypt(1)

View File

@ -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