Skip to content

Commit 7eabaee

Browse files
committedMar 3, 2025··
Release 3.5.0
* Silence warnings when collecting alt files (#521) * Adjust handling of encrypt patterns to match 3.3.0 and older * Make encrypt exclude patterns only match encrypted files * Automatically exclude alt and template files (#234) * Support negative alt conditions (#365) * Handle filenames with space in bash completion (#341) * Add new yadm.filename template variable (#520)
2 parents 5648f8b + 4f4c5e2 commit 7eabaee

16 files changed

+440
-210
lines changed
 

‎CHANGES

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
3.5.0
2+
* Silence warnings when collecting alt files (#521)
3+
* Adjust handling of encrypt patterns to match 3.3.0 and older
4+
* Make encrypt exclude patterns only match encrypted files
5+
* Automatically exclude alt and template files (#234)
6+
* Support negative alt conditions (#365)
7+
* Handle filenames with space in bash completion (#341)
8+
* Add new yadm.filename template variable (#520)
9+
110
3.4.0
211
* Improve and harden alt file regeneration (#466)
312
* Fix "yadm config" in fish completion (#491)

‎CONTRIBUTORS

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Jonathan Daigle
99
Luis López
1010
Tin Lai
1111
Espen Henriksen
12+
AaronYoung5
1213
Cameron Eagans
1314
Klas Mellbourn
1415
James Clark

‎Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ testhost: require-docker .testyadm
123123
--hostname testhost \
124124
--rm -it \
125125
-v "$(CURDIR)/.testyadm:/bin/yadm:ro" \
126+
-v "$(CURDIR)/completion/bash/yadm:/usr/share/bash-completion/completions/yadm:ro" \
126127
$(IMAGE) \
127128
bash -l
128129

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ The star count helps others discover yadm.
7878
[master-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=master
7979
[master-commits]: https://github.com/yadm-dev/yadm/commits/master
8080
[master-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/master.svg?label=master
81-
[obs-badge]: https://img.shields.io/badge/OBS-v3.4.0-blue
81+
[obs-badge]: https://img.shields.io/badge/OBS-v3.5.0-blue
8282
[obs-link]: https://software.opensuse.org/download.html?project=home%3ATheLocehiliosan%3Ayadm&package=yadm
8383
[releases-badge]: https://img.shields.io/github/tag/yadm-dev/yadm.svg?label=latest+release
8484
[releases-link]: https://github.com/yadm-dev/yadm/releases

‎completion/bash/yadm

+28-39
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,85 @@
11
# test if git completion is missing, but loader exists, attempt to load
2-
if ! declare -F _git > /dev/null && ! declare -F __git_wrap__git_main > /dev/null; then
3-
if declare -F _completion_loader > /dev/null; then
2+
if ! declare -F _git >/dev/null && ! declare -F __git_wrap__git_main >/dev/null; then
3+
if declare -F _completion_loader >/dev/null; then
44
_completion_loader git
55
fi
66
fi
77

88
# only operate if git completion is present
9-
if declare -F _git > /dev/null || declare -F __git_wrap__git_main > /dev/null; then
9+
if declare -F _git >/dev/null || declare -F __git_wrap__git_main >/dev/null; then
1010

1111
_yadm() {
1212

1313
local current=${COMP_WORDS[COMP_CWORD]}
1414
local penultimate
15-
if [ "$((COMP_CWORD-1))" -ge "0" ]; then
16-
penultimate=${COMP_WORDS[COMP_CWORD-1]}
15+
if ((COMP_CWORD >= 1)); then
16+
penultimate=${COMP_WORDS[COMP_CWORD - 1]}
1717
fi
1818
local antepenultimate
19-
if [ "$((COMP_CWORD-2))" -ge "0" ]; then
20-
antepenultimate=${COMP_WORDS[COMP_CWORD-2]}
19+
if ((COMP_CWORD >= 2)); then
20+
antepenultimate=${COMP_WORDS[COMP_CWORD - 2]}
2121
fi
2222

2323
local -x GIT_DIR
24-
# shellcheck disable=SC2034
2524
GIT_DIR="$(yadm introspect repo 2>/dev/null)"
2625

2726
case "$penultimate" in
2827
bootstrap)
2928
COMPREPLY=()
3029
return 0
31-
;;
30+
;;
3231
config)
33-
COMPREPLY=( $(compgen -W "$(yadm introspect configs 2>/dev/null)") )
32+
COMPREPLY=($(compgen -W "$(yadm introspect configs 2>/dev/null)"))
3433
return 0
35-
;;
34+
;;
3635
decrypt)
37-
COMPREPLY=( $(compgen -W "-l" -- "$current") )
36+
COMPREPLY=($(compgen -W "-l" -- "$current"))
3837
return 0
39-
;;
38+
;;
4039
init)
41-
COMPREPLY=( $(compgen -W "-f -w" -- "$current") )
40+
COMPREPLY=($(compgen -W "-f -w" -- "$current"))
4241
return 0
43-
;;
42+
;;
4443
introspect)
45-
COMPREPLY=( $(compgen -W "commands configs repo switches" -- "$current") )
44+
COMPREPLY=($(compgen -W "commands configs repo switches" -- "$current"))
4645
return 0
47-
;;
46+
;;
4847
help)
4948
COMPREPLY=() # no specific help yet
5049
return 0
51-
;;
50+
;;
5251
list)
53-
COMPREPLY=( $(compgen -W "-a" -- "$current") )
52+
COMPREPLY=($(compgen -W "-a" -- "$current"))
5453
return 0
55-
;;
54+
;;
5655
esac
5756

5857
case "$antepenultimate" in
5958
clone)
60-
COMPREPLY=( $(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current") )
59+
COMPREPLY=($(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current"))
6160
return 0
62-
;;
61+
;;
6362
esac
6463

65-
local yadm_switches=( $(yadm introspect switches 2>/dev/null) )
64+
local yadm_switches=($(yadm introspect switches 2>/dev/null))
6665

6766
# this condition is so files are completed properly for --yadm-xxx options
6867
if [[ " ${yadm_switches[*]} " != *" $penultimate "* ]]; then
6968
# TODO: somehow solve the problem with [--yadm-xxx option] being
7069
# incompatible with what git expects, namely [--arg=option]
71-
if declare -F _git > /dev/null; then
70+
if declare -F _git >/dev/null; then
7271
_git
7372
else
7473
__git_wrap__git_main
7574
fi
7675
fi
7776
if [[ "$current" =~ ^- ]]; then
78-
local matching
79-
matching=$(compgen -W "${yadm_switches[*]}" -- "$current")
80-
__gitcompappend "$matching"
77+
__gitcompappend "${yadm_switches[*]}" "" "$current" " "
8178
fi
8279

8380
# Find the index of where the sub-command argument should go.
8481
local command_idx
85-
for (( command_idx=1 ; command_idx < ${#COMP_WORDS[@]} ; command_idx++ )); do
82+
for ((command_idx = 1; command_idx < ${#COMP_WORDS[@]}; command_idx++)); do
8683
local command_idx_arg="${COMP_WORDS[$command_idx]}"
8784
if [[ " ${yadm_switches[*]} " = *" $command_idx_arg "* ]]; then
8885
let command_idx++
@@ -93,19 +90,11 @@ if declare -F _git > /dev/null || declare -F __git_wrap__git_main > /dev/null; t
9390
fi
9491
done
9592
if [[ "$COMP_CWORD" = "$command_idx" ]]; then
96-
local matching
97-
matching=$(compgen -W "$(yadm introspect commands 2>/dev/null)" -- "$current")
98-
__gitcompappend "$matching"
93+
__gitcompappend "$(yadm introspect commands 2>/dev/null)" "" "$current" " "
9994
fi
100-
101-
# remove duplicates found in COMPREPLY (a native bash way could be better)
102-
if [ -n "${COMPREPLY[*]}" ]; then
103-
COMPREPLY=($(echo "${COMPREPLY[@]}" | sort -u))
104-
fi
105-
10695
}
10796

108-
complete -o bashdefault -o default -F _yadm yadm 2>/dev/null \
109-
|| complete -o default -F _yadm yadm
97+
complete -o bashdefault -o default -o nospace -F _yadm yadm 2>/dev/null ||
98+
complete -o default -o nospace -F _yadm yadm
11099

111100
fi

‎test/conftest.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -609,14 +609,14 @@ def gnupg(tmpdir_factory, runner):
609609
env["GNUPGHOME"] = home
610610

611611
# this pre-populates std files in the GNUPGHOME
612-
runner(["gpg", "-k"], env=env)
612+
runner(["gpg", "-k"], env=env, report=False)
613613

614614
def register_gpg_password(password):
615615
"""Publish a new GPG mock password and flush cached passwords"""
616616
home.join("mock-password").write(password)
617-
runner(["gpgconf", "--reload", "gpg-agent"], env=env)
617+
runner(["gpgconf", "--reload", "gpg-agent"], env=env, report=False)
618618

619619
yield data(home, register_gpg_password)
620620

621-
runner(["gpgconf", "--kill", "gpg-agent"], env=env)
622-
runner(["gpgconf", "--remove-socketdir", "gpg-agent"], env=env)
621+
runner(["gpgconf", "--kill", "gpg-agent"], env=env, report=False)
622+
runner(["gpgconf", "--remove-socketdir", "gpg-agent"], env=env, report=False)

‎test/test_alt.py

+23
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,29 @@ def test_auto_alt(runner, yadm_cmd, paths, autoalt):
217217
assert str(paths.work.join(source_file)) not in linked
218218

219219

220+
@pytest.mark.usefixtures("ds1_copy")
221+
@pytest.mark.parametrize("autoexclude", [None, "true", "false"])
222+
def test_alt_exclude(runner, yadm_cmd, paths, autoexclude):
223+
"""Test alt exclude"""
224+
225+
# set the value of auto-exclude
226+
if autoexclude:
227+
os.system(" ".join(yadm_cmd("config", "yadm.auto-exclude", autoexclude)))
228+
229+
utils.create_alt_files(paths, "##default")
230+
run = runner(yadm_cmd("alt", "-d"))
231+
assert run.success
232+
233+
run = runner(yadm_cmd("status", "-z", "-uall", "--ignored"))
234+
assert run.success
235+
assert run.err == ""
236+
status = run.out.split("\0")
237+
238+
for link_path in TEST_PATHS:
239+
flags = "??" if autoexclude == "false" else "!!"
240+
assert f"{flags} {link_path}" in status
241+
242+
220243
@pytest.mark.usefixtures("ds1_copy")
221244
def test_stale_link_removal(runner, yadm_cmd, paths):
222245
"""Stale links to alternative files are removed

‎test/test_encryption.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def encrypt_targets(yadm_cmd, paths):
9292
paths.work.join("globs dir/globs file2").write("globs file2")
9393
expected.append("globs dir/globs file2")
9494
paths.encrypt.write("globs*\n", mode="a")
95+
paths.encrypt.write("globs d*/globs*\n", mode="a")
9596

9697
# blank lines
9798
paths.encrypt.write("\n \n\t\n", mode="a")
@@ -404,8 +405,8 @@ def test_encrypt_added_to_exclude(runner, yadm_cmd, paths, gnupg):
404405

405406
run = runner(yadm_cmd("encrypt"), env=env)
406407

407-
assert "test-encrypt-data" in paths.repo.join("info/exclude").read()
408-
assert "original-data" in paths.repo.join("info/exclude").read()
408+
assert "test-encrypt-data" in exclude_file.read()
409+
assert "original-data" in exclude_file.read()
409410
assert run.success
410411
assert run.err == ""
411412

‎test/test_unit_exclude_encrypted.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude):
1010
"""Test exclude_encrypted()"""
1111

12-
header = "# yadm-auto-excludes\n# This section is managed by yadm.\n# Any edits below will be lost.\n"
12+
header = """\
13+
# yadm-auto-excludes
14+
# This section is managed by yadm.
15+
# Any edits below will be lost.
16+
# yadm encrypt
17+
"""
1318

1419
config_function = 'function config() { echo "false";}'
1520
if auto_exclude:
@@ -24,7 +29,7 @@ def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, e
2429
if exclude == "outdated":
2530
exclude_file.write(f"original-exclude\n{header}outdated\n", ensure=True)
2631
elif exclude == "up-to-date":
27-
exclude_file.write(f"original-exclude\n{header}test-encrypt-data\n", ensure=True)
32+
exclude_file.write(f"original-exclude\n{header}/test-encrypt-data\n", ensure=True)
2833

2934
script = f"""
3035
YADM_TEST=1 source {yadm}
@@ -42,9 +47,9 @@ def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, e
4247
if encrypt_exists:
4348
assert exclude_file.exists()
4449
if exclude == "missing":
45-
assert exclude_file.read() == f"{header}test-encrypt-data\n"
50+
assert exclude_file.read() == f"{header}/test-encrypt-data\n"
4651
else:
47-
assert exclude_file.read() == ("original-exclude\n" f"{header}test-encrypt-data\n")
52+
assert exclude_file.read() == ("original-exclude\n" f"{header}/test-encrypt-data\n")
4853
if exclude != "up-to-date":
4954
assert f"Updating {exclude_file}" in run.out
5055
else:

‎test/test_unit_parse_encrypt.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,11 @@ def create_test_encrypt_data(paths):
100100
edata += "*card1\n" # matches same file as the one above
101101
paths.work.join("wildcard1").write("", ensure=True)
102102
paths.work.join("wildcard2").write("", ensure=True)
103+
paths.work.join("subdir/wildcard1").write("", ensure=True)
103104
expected.add("wildcard1")
104105
expected.add("wildcard2")
105106

106-
edata += "dirwild*\n"
107+
edata += "dirwild*/file*\n"
107108
paths.work.join("dirwildcard/file1").write("", ensure=True)
108109
paths.work.join("dirwildcard/file2").write("", ensure=True)
109110
expected.add("dirwildcard/file1")
@@ -125,6 +126,9 @@ def create_test_encrypt_data(paths):
125126
expected.add("ex ex/file4")
126127
expected.add("ex ex/file6.text")
127128

129+
paths.work.join("dirwildcard/file7.ex").write("", ensure=True)
130+
expected.add("dirwildcard/file7.ex")
131+
128132
# double star
129133
edata += "doublestar/**/file*\n"
130134
edata += "!**/file3\n"

‎test/test_unit_score_file.py

+73
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,76 @@ def test_underscores_and_upper_case_in_distro_and_family(runner, yadm):
321321
assert run.success
322322
assert run.err == ""
323323
assert run.out == expected
324+
325+
326+
def test_negative_class_condition(runner, yadm):
327+
"""Test negative class condition: returns 0 when matching and proper score when not matching."""
328+
script = f"""
329+
YADM_TEST=1 source {yadm}
330+
local_class="testclass"
331+
local_classes=("testclass")
332+
333+
# 0
334+
score=0
335+
score_file "filename##~class.testclass" "dest"
336+
echo "score: $score"
337+
338+
# 16
339+
score=0
340+
score_file "filename##~class.badclass" "dest"
341+
echo "score2: $score"
342+
343+
# 16
344+
score=0
345+
score_file "filename##~c.badclass" "dest"
346+
echo "score3: $score"
347+
"""
348+
run = runner(command=["bash"], inp=script)
349+
assert run.success
350+
output = run.out.strip().splitlines()
351+
assert output[0] == "score: 0"
352+
assert output[1] == "score2: 16"
353+
assert output[2] == "score3: 16"
354+
355+
356+
def test_negative_combined_conditions(runner, yadm):
357+
"""Test negative conditions for multiple alt types: returns 0 when matching and proper score when not matching."""
358+
script = f"""
359+
YADM_TEST=1 source {yadm}
360+
local_class="testclass"
361+
local_classes=("testclass")
362+
local_distro="testdistro"
363+
364+
# (0) + (0) = 0
365+
score=0
366+
score_file "filename##~class.testclass,~distro.testdistro" "dest"
367+
echo "score: $score"
368+
369+
# (1000 + 16) + (1000 + 4) = 2020
370+
score=0
371+
score_file "filename##class.testclass,distro.testdistro" "dest"
372+
echo "score2: $score"
373+
374+
# 0 (negated class condition)
375+
score=0
376+
score_file "filename##~class.badclass,~distro.testdistro" "dest"
377+
echo "score3: $score"
378+
379+
# (1000 + 16) + (4) = 1020
380+
score=0
381+
score_file "filename##class.testclass,~distro.baddistro" "dest"
382+
echo "score4: $score"
383+
384+
# (1000 + 16) + (16) = 1032
385+
score=0
386+
score_file "filename##class.testclass,~class.badclass" "dest"
387+
echo "score5: $score"
388+
"""
389+
run = runner(command=["bash"], inp=script)
390+
assert run.success
391+
output = run.out.strip().splitlines()
392+
assert output[0] == "score: 0"
393+
assert output[1] == "score2: 2020"
394+
assert output[2] == "score3: 0"
395+
assert output[3] == "score4: 1020"
396+
assert output[4] == "score5: 1032"

‎test/test_unit_template_default.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141

142142
INCLUDE_BASIC = "basic\n"
143143
INCLUDE_VARIABLES = """\
144-
included <{{ yadm.class }}> file
144+
included <{{ yadm.class }}> file ({{yadm.filename}})
145145
146146
empty line above
147147
"""
@@ -151,8 +151,8 @@
151151
The first line
152152
{% include empty %}
153153
An empty file removes the line above
154-
{%include basic%}
155-
{% include "./variables.{{ yadm.os }}" %}
154+
{%include ./basic%}
155+
{% include "variables.{{ yadm.os }}" %}
156156
{% include dir/nested %}
157157
Include basic again:
158158
{% include basic %}
@@ -161,7 +161,7 @@
161161
The first line
162162
An empty file removes the line above
163163
basic
164-
included <{LOCAL_CLASS}> file
164+
included <{LOCAL_CLASS}> file (VARIABLES_FILENAME)
165165
166166
empty line above
167167
no newline at the end
@@ -280,6 +280,8 @@ def test_include(runner, yadm, tmpdir):
280280
input_file.chmod(FILE_MODE)
281281
output_file = tmpdir.join("output")
282282

283+
expected = EXPECTED_INCLUDE.replace("VARIABLES_FILENAME", str(variables_file))
284+
283285
script = f"""
284286
YADM_TEST=1 source {yadm}
285287
set_awk
@@ -290,7 +292,7 @@ def test_include(runner, yadm, tmpdir):
290292
run = runner(command=["bash"], inp=script)
291293
assert run.success
292294
assert run.err == ""
293-
assert output_file.read() == EXPECTED_INCLUDE
295+
assert output_file.read() == expected
294296
assert os.stat(output_file).st_mode == os.stat(input_file).st_mode
295297

296298

‎yadm

+136-61
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ if [ -z "$BASH_VERSION" ]; then
2222
[ "$YADM_TEST" != 1 ] && exec bash "$0" "$@"
2323
fi
2424

25-
VERSION=3.4.0
25+
VERSION=3.5.0
2626

2727
YADM_WORK="$HOME"
2828
YADM_DIR=
@@ -61,6 +61,7 @@ PROC_VERSION="/proc/version"
6161
OPERATING_SYSTEM="Unknown"
6262

6363
ENCRYPT_INCLUDE_FILES="unparsed"
64+
NO_ENCRYPT_TRACKED_FILES=()
6465

6566
LEGACY_WARNING_ISSUED=0
6667
INVALID_ALT=()
@@ -179,39 +180,50 @@ function score_file() {
179180
local value=${field#*.}
180181
[ "$field" = "$label" ] && value="" # when .value is omitted
181182

183+
# Check for negative condition prefix (e.g., "~<label>")
184+
local negate=0
185+
if [ "${label:0:1}" = "~" ]; then
186+
negate=1
187+
label="${label:1}"
188+
fi
189+
182190
shopt -s nocasematch
183-
local -i delta=-1
191+
local -i delta=$((negate ? 1 : -1))
184192
case "$label" in
185193
default)
186-
delta=0
194+
if ((negate)); then
195+
INVALID_ALT+=("$source")
196+
else
197+
delta=0
198+
fi
187199
;;
188200
a | arch)
189-
[[ "$value" = "$local_arch" ]] && delta=1
201+
[[ "$value" = "$local_arch" ]] && delta=1 || delta=-1
190202
;;
191203
o | os)
192-
[[ "$value" = "$local_system" ]] && delta=2
204+
[[ "$value" = "$local_system" ]] && delta=2 || delta=-2
193205
;;
194206
d | distro)
195-
[[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4
207+
[[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 || delta=-4
196208
;;
197209
f | distro_family)
198-
[[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8
210+
[[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 || delta=-8
199211
;;
200212
c | class)
201-
in_list "$value" "${local_classes[@]}" && delta=16
213+
in_list "$value" "${local_classes[@]}" && delta=16 || delta=-16
202214
;;
203215
h | hostname)
204-
[[ "$value" = "$local_host" ]] && delta=32
216+
[[ "$value" = "$local_host" ]] && delta=32 || delta=-32
205217
;;
206218
u | user)
207-
[[ "$value" = "$local_user" ]] && delta=64
219+
[[ "$value" = "$local_user" ]] && delta=64 || delta=-64
208220
;;
209221
e | extension)
210222
# extension isn't a condition and doesn't affect the score
211223
continue
212224
;;
213225
t | template | yadm)
214-
if [ -d "$source" ]; then
226+
if [ -d "$source" ] || ((negate)); then
215227
INVALID_ALT+=("$source")
216228
else
217229
template_processor=$(choose_template_processor "$value")
@@ -230,11 +242,12 @@ function score_file() {
230242
esac
231243
shopt -u nocasematch
232244

245+
((negate)) && delta=$((-delta))
233246
if ((delta < 0)); then
234247
score=0
235248
return
236249
fi
237-
score=$((score + 1000 + delta))
250+
score=$((score + delta + (negate ? 0 : 1000)))
238251
done
239252

240253
record_score "$score" "$target" "$source" "$template_processor"
@@ -366,7 +379,7 @@ BEGIN {
366379
yadm["user"] = user
367380
yadm["distro"] = distro
368381
yadm["distro_family"] = distro_family
369-
yadm["source"] = source
382+
yadm["source"] = ARGV[1]
370383
371384
VARIABLE = "(env|yadm)\\.[a-zA-Z0-9_]+"
372385
@@ -456,6 +469,9 @@ function replace_vars(input) {
456469
if (fields[1] == "env") {
457470
output = output ENVIRON[fields[2]]
458471
}
472+
else if (fields[2] == "filename") {
473+
output = output filename[current]
474+
}
459475
else {
460476
output = output yadm[fields[2]]
461477
}
@@ -472,7 +488,6 @@ EOF
472488
-v user="$local_user" \
473489
-v distro="$local_distro" \
474490
-v distro_family="$local_distro_family" \
475-
-v source="$input" \
476491
-v source_dir="$(builtin_dirname "$input")" \
477492
"$awk_pgm" \
478493
"$input" "${local_classes[@]}"
@@ -690,6 +705,11 @@ function set_local_alt_values() {
690705
}
691706

692707
function alt_linking() {
708+
local -a exclude=()
709+
710+
local log="debug"
711+
[ -n "$loud" ] && log="echo"
712+
693713
local -i index
694714
for ((index = 0; index < ${#alt_targets[@]}; ++index)); do
695715
local target="${alt_targets[$index]}"
@@ -708,17 +728,17 @@ function alt_linking() {
708728
if [[ -n "$template_processor" ]]; then
709729
template "$template_processor" "$source" "$target"
710730
elif [[ "$do_copy" -eq 1 ]]; then
711-
debug "Copying $source to $target"
712-
[[ -n "$loud" ]] && echo "Copying $source to $target"
713-
731+
$log "Copying $source to $target"
714732
cp -f "$source" "$target"
715733
else
716-
debug "Linking $source to $target"
717-
[[ -n "$loud" ]] && echo "Linking $source to $target"
718-
734+
$log "Linking $source to $target"
719735
ln_relative "$source" "$target"
720736
fi
737+
738+
exclude+=("${target#"$YADM_WORK"}")
721739
done
740+
741+
update_exclude alt "${exclude[@]}"
722742
}
723743

724744
function ln_relative() {
@@ -1042,6 +1062,12 @@ function encrypt() {
10421062
printf '%s\n' "${ENCRYPT_INCLUDE_FILES[@]}"
10431063
echo
10441064

1065+
if [ ${#NO_ENCRYPT_TRACKED_FILES[@]} -gt 0 ]; then
1066+
echo "Warning: The following files are tracked and will NOT be encrypted:"
1067+
printf '%s\n' "${NO_ENCRYPT_TRACKED_FILES[@]}"
1068+
echo
1069+
fi
1070+
10451071
# encrypt all files which match the globs
10461072
if tar -f - -c "${ENCRYPT_INCLUDE_FILES[@]}" | _encrypt_to "$YADM_ARCHIVE"; then
10471073
echo "Wrote new file: $YADM_ARCHIVE"
@@ -1466,52 +1492,91 @@ function version() {
14661492

14671493
# ****** Utility Functions ******
14681494

1469-
function exclude_encrypted() {
1495+
function update_exclude() {
14701496

1497+
local auto_exclude
14711498
auto_exclude=$(config --bool yadm.auto-exclude)
14721499
[ "$auto_exclude" == "false" ] && return 0
14731500

1474-
exclude_path="${YADM_REPO}/info/exclude"
1475-
newline=$'\n'
1476-
exclude_flag="# yadm-auto-excludes"
1477-
exclude_header="${exclude_flag}${newline}"
1501+
local exclude_path="${YADM_REPO}/info/exclude"
1502+
local newline=$'\n'
1503+
1504+
local part_path="$exclude_path.yadm-$1"
1505+
local part_str
1506+
part_str=$(join_string "$newline" "${@:2}")
1507+
1508+
if [ -e "$part_path" ]; then
1509+
if [ "$part_str" = "$(<"$part_path")" ]; then
1510+
return
1511+
fi
1512+
1513+
rm -f "$part_path"
1514+
elif [ -z "$part_str" ]; then
1515+
return
1516+
fi
1517+
1518+
if [ -n "$part_str" ]; then
1519+
assert_parent "$part_path"
1520+
cat >"$part_path" <<<"$part_str"
1521+
fi
1522+
1523+
local exclude_flag="# yadm-auto-excludes"
1524+
1525+
local exclude_header="${exclude_flag}${newline}"
14781526
exclude_header="${exclude_header}# This section is managed by yadm."
14791527
exclude_header="${exclude_header}${newline}"
14801528
exclude_header="${exclude_header}# Any edits below will be lost."
14811529
exclude_header="${exclude_header}${newline}"
14821530

1483-
# do nothing if there is no YADM_ENCRYPT
1484-
[ -e "$YADM_ENCRYPT" ] || return 0
1485-
1486-
# read encrypt
1487-
encrypt_data=""
1488-
while IFS='' read -r line || [ -n "$line" ]; do
1489-
encrypt_data="${encrypt_data}${line}${newline}"
1490-
done <"$YADM_ENCRYPT"
1491-
14921531
# read info/exclude
1493-
unmanaged=""
1494-
managed=""
1532+
local unmanaged=""
1533+
local managed=""
14951534
if [ -e "$exclude_path" ]; then
1496-
flag_seen=0
1535+
local -i flag_seen=0
1536+
local line
14971537
while IFS='' read -r line || [ -n "$line" ]; do
14981538
[ "$line" = "$exclude_flag" ] && flag_seen=1
1499-
if [ "$flag_seen" -eq 0 ]; then
1500-
unmanaged="${unmanaged}${line}${newline}"
1501-
else
1539+
if ((flag_seen)); then
15021540
managed="${managed}${line}${newline}"
1541+
else
1542+
unmanaged="${unmanaged}${line}${newline}"
15031543
fi
15041544
done <"$exclude_path"
15051545
fi
15061546

1507-
if [ "${exclude_header}${encrypt_data}" != "$managed" ]; then
1547+
local exclude_str=""
1548+
for suffix in alt encrypt; do
1549+
if [ -e "${exclude_path}.yadm-$suffix" ]; then
1550+
local header="# yadm $suffix$newline"
1551+
exclude_str="$exclude_str$header$(<"$exclude_path".yadm-"$suffix")"
1552+
fi
1553+
done
1554+
1555+
if [ "${exclude_header}${exclude_str}${newline}" != "$managed" ]; then
15081556
debug "Updating ${exclude_path}"
1509-
assert_parent "$exclude_path"
1510-
printf "%s" "${unmanaged}${exclude_header}${encrypt_data}" >"$exclude_path"
1557+
cat >"$exclude_path" <<<"${unmanaged}${exclude_header}${exclude_str}"
15111558
fi
15121559

15131560
return 0
1561+
}
1562+
1563+
function exclude_encrypted() {
1564+
local -a exclude=()
1565+
1566+
if [ -r "$YADM_ENCRYPT" ]; then
1567+
local pattern
1568+
while IFS='' read -r pattern || [ -n "$pattern" ]; do
1569+
# Prepend / to the pattern so that it matches the same files as in
1570+
# parse_encrypt (i.e. only from the root)
1571+
if [ "${pattern:0:1}" = "!" ]; then
1572+
exclude+=("!/${pattern:1}")
1573+
elif ! [[ $pattern =~ ^[[:blank:]]*(#|$) ]]; then
1574+
exclude+=("/$pattern")
1575+
fi
1576+
done <"$YADM_ENCRYPT"
1577+
fi
15141578

1579+
update_exclude encrypt "${exclude[@]}"
15151580
}
15161581

15171582
function query_distro() {
@@ -1926,26 +1991,36 @@ function parse_encrypt() {
19261991
local -a exclude
19271992
local -a include
19281993

1929-
while IFS= read -r pattern; do
1930-
case $pattern in
1931-
\#*)
1932-
# Ignore comments
1933-
;;
1934-
!*)
1935-
exclude+=("--exclude=${pattern:1}")
1936-
;;
1937-
*)
1938-
if ! [[ $pattern =~ ^[[:blank:]]*$ ]]; then
1939-
include+=("$pattern")
1940-
fi
1941-
;;
1942-
esac
1994+
local pattern
1995+
while IFS='' read -r pattern || [ -n "$pattern" ]; do
1996+
if [ "${pattern:0:1}" = "!" ]; then
1997+
exclude+=("--exclude=/${pattern:1}")
1998+
elif ! [[ $pattern =~ ^[[:blank:]]*(#|$) ]]; then
1999+
include+=("$pattern")
2000+
fi
19432001
done <"$YADM_ENCRYPT"
19442002

1945-
if [[ ${#include} -gt 0 ]]; then
1946-
while IFS= read -r filename; do
1947-
ENCRYPT_INCLUDE_FILES+=("${filename%/}")
1948-
done <<<"$("$GIT_PROGRAM" ls-files --others "${exclude[@]}" -- "${include[@]}")"
2003+
if [ ${#include[@]} -gt 0 ]; then
2004+
while IFS='' read -r filename; do
2005+
if [ -n "$filename" ]; then
2006+
ENCRYPT_INCLUDE_FILES+=("${filename%/}")
2007+
fi
2008+
done <<<"$(
2009+
"$GIT_PROGRAM" --glob-pathspecs ls-files --others \
2010+
"${exclude[@]}" -- "${include[@]}" 2>/dev/null
2011+
)"
2012+
2013+
[ "$YADM_COMMAND" = "encrypt" ] || return
2014+
2015+
# List files that matches encryption pattern but is tracked
2016+
while IFS='' read -r filename; do
2017+
if [ -n "$filename" ]; then
2018+
NO_ENCRYPT_TRACKED_FILES+=("${filename%/}")
2019+
fi
2020+
done <<<"$(
2021+
"$GIT_PROGRAM" --glob-pathspecs ls-files \
2022+
"${exclude[@]}" -- "${include[@]}"
2023+
)"
19492024
fi
19502025
}
19512026

‎yadm.1

+39-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\" vim: set spell so=8:
2-
.TH YADM 1 "February 9, 2025" "3.4.0"
2+
.TH YADM 1 "March 3, 2025" "3.5.0"
33

44
.SH NAME
55

@@ -363,7 +363,8 @@ you may still run "yadm alt" manually to create the alternate links. This
363363
feature is enabled by default.
364364
.TP
365365
.B yadm.auto-exclude
366-
Disable the automatic exclusion of patterns defined in
366+
Disable the automatic exclusion of created alternate links, template files and
367+
patterns defined in
367368
.IR $HOME/.config/yadm/encrypt .
368369
This feature is enabled by default.
369370
.TP
@@ -475,9 +476,11 @@ commas.
475476

476477
Each condition is an attribute/value pair, separated by a period. Some
477478
conditions do not require a "value", and in that case, the period and value can
478-
be omitted. Most attributes can be abbreviated as a single letter.
479+
be omitted. Most attributes can be abbreviated as a single letter. Prefixing an
480+
attribute with "~" negates the condition, meaning the condition is considered
481+
only if the attribute/value pair evaluates to false.
479482

480-
<attribute>[.<value>]
483+
[~]<attribute>[.<value>]
481484

482485
.BR NOTE :
483486
Value is compared case-insensitive.
@@ -509,19 +512,19 @@ Class must be manually set using
509512
See the CONFIGURATION section for more details about setting
510513
.BR local.class .
511514
.TP
515+
.BR distro_family ,\ f
516+
Valid if the value matches the distro family.
517+
Distro family is calculated by inspecting the ID_LIKE line from
518+
.B "/etc/os-release"
519+
(or ID if no ID_LIKE line is found).
520+
.TP
512521
.BR distro ,\ d
513522
Valid if the value matches the distro.
514523
Distro is calculated by running
515524
.B "lsb_release \-si"
516525
or by inspecting the ID from
517526
.BR "/etc/os-release" .
518527
.TP
519-
.BR distro_family ,\ f
520-
Valid if the value matches the distro family.
521-
Distro family is calculated by inspecting the ID_LIKE line from
522-
.B "/etc/os-release"
523-
(or ID if no ID_LIKE line is found).
524-
.TP
525528
.BR os ,\ o
526529
Valid if the value matches the OS.
527530
OS is calculated by running
@@ -554,9 +557,10 @@ symbolic links will be created for the most appropriate version.
554557

555558
The "most appropriate" version is determined by calculating a score for each
556559
version of a file. A template is always scored higher than any symlink
557-
condition. The number of conditions is the next largest factor in scoring.
558-
Files with more conditions will always be favored. Any invalid condition will
559-
disqualify that file completely.
560+
condition. The number of conditions is the next largest factor in scoring;
561+
files with more conditions will always be favored. Negative conditions (prefixed
562+
with "~") are scored only relative to the number of non-negated conditions.
563+
Any invalid condition will disqualify that file completely.
560564

561565
If you don't care to have all versions of alternates stored in the same
562566
directory as the generated symlink, you can place them in the
@@ -569,6 +573,7 @@ files are managed by yadm's repository:
569573

570574
- $HOME/path/example.txt##default
571575
- $HOME/path/example.txt##class.Work
576+
- $HOME/path/example.txt##class.Work,~os.Darwin
572577
- $HOME/path/example.txt##os.Darwin
573578
- $HOME/path/example.txt##os.Darwin,hostname.host1
574579
- $HOME/path/example.txt##os.Darwin,hostname.host2
@@ -597,10 +602,17 @@ If running on a Solaris server, the link will use the default version:
597602

598603
.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##default
599604

600-
If running on a system, with class set to "Work", the link will be:
605+
If running on a Macbook with class set to "Work", the link will be:
601606

602607
.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##class.Work
603608

609+
Since class has higher precedence than os, this version is chosen.
610+
611+
If running on a system with class set to "Work", but instead within Windows
612+
Subsystem for Linux, where the os is reported as WSL, the link will be:
613+
614+
.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##class.Work,~os.Darwin
615+
604616
If no "##default" version exists and no files have valid conditions, then no
605617
link will be created.
606618

@@ -614,6 +626,12 @@ configuration.
614626
Even if disabled, links can be manually created by running
615627
.BR "yadm alt" .
616628

629+
Created links are automatically added to the repository's
630+
.I info/exclude
631+
file. This can be disabled using the
632+
.I yadm.auto-exclude
633+
configuration.
634+
617635
Class is a special value which is stored locally on each host (inside the local
618636
repository). To use alternate symlinks using class, you must set the value of
619637
class using the configuration
@@ -687,6 +705,7 @@ During processing, the following variables are available in the template:
687705
yadm.classes YADM_CLASSES All classes
688706
yadm.distro YADM_DISTRO lsb_release \-si
689707
yadm.distro_family YADM_DISTRO_FAMILY ID_LIKE from /etc/os-release
708+
yadm.filename Filename for the current file
690709
yadm.hostname YADM_HOSTNAME uname \-n (without domain)
691710
yadm.os YADM_OS uname \-s
692711
yadm.source YADM_SOURCE Template filename
@@ -748,6 +767,12 @@ would look like:
748767
<%+ whatever.extra %>
749768
<% fi -%>
750769

770+
Created files are automatically added to the repository's
771+
.I info/exclude
772+
file. This can be disabled using the
773+
.I yadm.auto-exclude
774+
configuration.
775+
751776
.SH ENCRYPTION
752777

753778
It can be useful to manage confidential files, like SSH or GPG keys, across

‎yadm.md

+100-78
Large diffs are not rendered by default.

‎yadm.spec

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
%{!?_pkgdocdir: %global _pkgdocdir %{_docdir}/%{name}-%{version}}
22
Name: yadm
33
Summary: Yet Another Dotfiles Manager
4-
Version: 3.4.0
4+
Version: 3.5.0
55
Group: Development/Tools
66
Release: 1%{?dist}
77
URL: https://yadm.io

0 commit comments

Comments
 (0)
Please sign in to comment.