1
0
mirror of https://github.com/TheLocehiliosan/yadm synced 2025-06-07 01:53:59 +00:00

Change handling of dirs with alt conditions

Instead of creating symlinks pointing at the directory, create individual
symlinks for each file within the dir alternate (fixes #490).

Also rework how stale symlinks are removed. Now a (stale) symlink will only be
removed if it's pointing at a file that's an altnerate file (fixes #236).
This commit is contained in:
Erik Flodin 2025-01-27 21:06:34 +01:00
parent 7f76a455bb
commit 4214de8d91
No known key found for this signature in database
GPG Key ID: 420A7C865EE3F85F
6 changed files with 244 additions and 233 deletions

View File

@ -40,15 +40,15 @@ def test_alt_source(runner, paths, tracked, encrypt, exclude, yadm_alt):
source_file_content = link_path + "##default" source_file_content = link_path + "##default"
source_file = basepath.join(source_file_content) source_file = basepath.join(source_file_content)
link_file = paths.work.join(link_path) link_file = paths.work.join(link_path)
if link_path == utils.ALT_DIR:
source_file = source_file.join(utils.CONTAINED)
link_file = link_file.join(utils.CONTAINED)
if tracked or (encrypt and not exclude): if tracked or (encrypt and not exclude):
assert link_file.islink() assert link_file.islink()
target = py.path.local(os.path.realpath(link_file)) target = py.path.local(os.path.realpath(link_file))
if target.isfile(): assert target.isfile()
assert link_file.read() == source_file_content assert link_file.read() == source_file_content
assert str(source_file) in linked assert str(source_file) in linked
else:
assert link_file.join(utils.CONTAINED).read() == source_file_content
assert str(source_file) in linked
else: else:
assert not link_file.exists() assert not link_file.exists()
assert str(source_file) not in linked assert str(source_file) not in linked
@ -73,6 +73,9 @@ def test_relative_link(runner, paths, yadm_alt):
source_file_content = link_path + "##default" source_file_content = link_path + "##default"
source_file = basepath.join(source_file_content) source_file = basepath.join(source_file_content)
link_file = paths.work.join(link_path) link_file = paths.work.join(link_path)
if link_path == utils.ALT_DIR:
source_file = source_file.join(utils.CONTAINED)
link_file = link_file.join(utils.CONTAINED)
link = link_file.readlink() link = link_file.readlink()
relpath = os.path.relpath(source_file, start=os.path.dirname(link_file)) relpath = os.path.relpath(source_file, start=os.path.dirname(link_file))
assert link == relpath assert link == relpath
@ -128,15 +131,17 @@ def test_alt_conditions(runner, paths, tst_arch, tst_sys, tst_distro, tst_distro
linked = utils.parse_alt_output(run.out) linked = utils.parse_alt_output(run.out)
for link_path in TEST_PATHS: for link_path in TEST_PATHS:
source_file = link_path + suffix source_file_content = link_path + suffix
assert paths.work.join(link_path).islink() source_file = paths.work.join(source_file_content)
target = py.path.local(os.path.realpath(paths.work.join(link_path))) link_file = paths.work.join(link_path)
if target.isfile(): if link_path == utils.ALT_DIR:
assert paths.work.join(link_path).read() == source_file source_file = source_file.join(utils.CONTAINED)
assert str(paths.work.join(source_file)) in linked link_file = link_file.join(utils.CONTAINED)
else: assert link_file.islink()
assert paths.work.join(link_path).join(utils.CONTAINED).read() == source_file target = py.path.local(os.path.realpath(link_file))
assert str(paths.work.join(source_file)) in linked assert target.isfile()
assert link_file.read() == source_file_content
assert str(source_file) in linked
@pytest.mark.usefixtures("ds1_copy") @pytest.mark.usefixtures("ds1_copy")
@ -156,6 +161,7 @@ def test_alt_templates(runner, paths, kind, label):
suffix = f"##{label}.{kind}" suffix = f"##{label}.{kind}"
if kind is None: if kind is None:
suffix = f"##{label}" suffix = f"##{label}"
utils.create_alt_files(paths, suffix) utils.create_alt_files(paths, suffix)
run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"])
assert run.success assert run.success
@ -163,11 +169,15 @@ def test_alt_templates(runner, paths, kind, label):
created = utils.parse_alt_output(run.out, linked=False) created = utils.parse_alt_output(run.out, linked=False)
for created_path in TEST_PATHS: for created_path in TEST_PATHS:
if created_path != utils.ALT_DIR: source_file_content = created_path + suffix
source_file = created_path + suffix source_file = paths.work.join(source_file_content)
assert paths.work.join(created_path).isfile() created_file = paths.work.join(created_path)
assert paths.work.join(created_path).read().strip() == source_file if created_path == utils.ALT_DIR:
assert str(paths.work.join(source_file)) in created source_file = source_file.join(utils.CONTAINED)
created_file = created_file.join(utils.CONTAINED)
assert created_file.isfile()
assert created_file.read().strip() == source_file_content
assert str(source_file) in created
@pytest.mark.usefixtures("ds1_copy") @pytest.mark.usefixtures("ds1_copy")
@ -201,20 +211,22 @@ def test_auto_alt(runner, yadm_cmd, paths, autoalt):
linked = utils.parse_alt_output(run.out) linked = utils.parse_alt_output(run.out)
for link_path in TEST_PATHS: for link_path in TEST_PATHS:
source_file = link_path + "##default" source_file_content = link_path + "##default"
source_file = paths.work.join(source_file_content)
link_file = paths.work.join(link_path)
if link_path == utils.ALT_DIR:
source_file = source_file.join(utils.CONTAINED)
link_file = link_file.join(utils.CONTAINED)
if autoalt == "false": if autoalt == "false":
assert not paths.work.join(link_path).exists() assert not link_file.exists()
else: else:
assert paths.work.join(link_path).islink() assert link_file.islink()
target = py.path.local(os.path.realpath(paths.work.join(link_path))) target = py.path.local(os.path.realpath(link_file))
if target.isfile(): assert target.isfile()
assert paths.work.join(link_path).read() == source_file assert link_file.read() == source_file_content
# no linking output when run via auto-alt # no linking output when run via auto-alt
assert str(paths.work.join(source_file)) not in linked assert str(source_file) not in linked
else:
assert paths.work.join(link_path).join(utils.CONTAINED).read() == source_file
# no linking output when run via auto-alt
assert str(paths.work.join(source_file)) not in linked
@pytest.mark.usefixtures("ds1_copy") @pytest.mark.usefixtures("ds1_copy")
@ -236,6 +248,8 @@ def test_alt_exclude(runner, yadm_cmd, paths, autoexclude):
status = run.out.split("\0") status = run.out.split("\0")
for link_path in TEST_PATHS: for link_path in TEST_PATHS:
if link_path == utils.ALT_DIR:
link_path = f"{link_path}/{utils.CONTAINED}"
flags = "??" if autoexclude == "false" else "!!" flags = "??" if autoexclude == "false" else "!!"
assert f"{flags} {link_path}" in status assert f"{flags} {link_path}" in status
@ -262,16 +276,18 @@ def test_stale_link_removal(runner, yadm_cmd, paths):
linked = utils.parse_alt_output(run.out) linked = utils.parse_alt_output(run.out)
# assert the proper linking has occurred # assert the proper linking has occurred
for stale_path in TEST_PATHS: for link_path in TEST_PATHS:
source_file = stale_path + "##class." + tst_class source_file_content = link_path + f"##class.{tst_class}"
assert paths.work.join(stale_path).islink() source_file = paths.work.join(source_file_content)
target = py.path.local(os.path.realpath(paths.work.join(stale_path))) link_file = paths.work.join(link_path)
if target.isfile(): if link_path == utils.ALT_DIR:
assert paths.work.join(stale_path).read() == source_file source_file = source_file.join(utils.CONTAINED)
assert str(paths.work.join(source_file)) in linked link_file = link_file.join(utils.CONTAINED)
else: assert link_file.islink()
assert paths.work.join(stale_path).join(utils.CONTAINED).read() == source_file target = py.path.local(os.path.realpath(link_file))
assert str(paths.work.join(source_file)) in linked assert target.isfile()
assert link_file.read() == source_file_content
assert str(source_file) in linked
# change the class so there are no valid alternates # change the class so there are no valid alternates
utils.set_local(paths, "class", "changedclass") utils.set_local(paths, "class", "changedclass")
@ -284,9 +300,53 @@ def test_stale_link_removal(runner, yadm_cmd, paths):
# assert the linking is removed # assert the linking is removed
for stale_path in TEST_PATHS: for stale_path in TEST_PATHS:
source_file = stale_path + "##class." + tst_class source_file_content = stale_path + f"##class.{tst_class}"
assert not paths.work.join(stale_path).exists() source_file = paths.work.join(source_file_content)
assert str(paths.work.join(source_file)) not in linked stale_file = paths.work.join(stale_path)
if stale_path == utils.ALT_DIR:
source_file = source_file.join(utils.CONTAINED)
stale_file = stale_file.join(utils.CONTAINED)
assert not stale_file.exists()
assert str(source_file) not in linked
@pytest.mark.usefixtures("ds1_copy")
def test_legacy_dir_link_removal(runner, yadm_cmd, paths):
"""Legacy link to alternative dir is removed
This test ensures that a legacy dir alternative (i.e. symlink to the dir
itself) is converted to indiividual links.
"""
utils.create_alt_files(paths, "##default")
# Create legacy link
link_dir = paths.work.join(utils.ALT_DIR)
link_dir.mksymlinkto(link_dir.basename + "##default")
assert link_dir.islink()
# run alt to trigger linking
run = runner(yadm_cmd("alt"))
assert run.success
assert run.err == ""
linked = utils.parse_alt_output(run.out)
# assert legacy link is removed
assert not link_dir.islink()
# assert the proper linking has occurred
for link_path in TEST_PATHS:
source_file_content = link_path + "##default"
source_file = paths.work.join(source_file_content)
link_file = paths.work.join(link_path)
if link_path == utils.ALT_DIR:
source_file = source_file.join(utils.CONTAINED)
link_file = link_file.join(utils.CONTAINED)
assert link_file.islink()
target = py.path.local(os.path.realpath(link_file))
assert target.isfile()
assert link_file.read() == source_file_content
assert str(source_file) in linked
@pytest.mark.usefixtures("ds1_repo_copy") @pytest.mark.usefixtures("ds1_repo_copy")

View File

@ -1,39 +0,0 @@
"""Unit tests: remove_stale_links"""
import os
import pytest
@pytest.mark.parametrize("linked", [True, False])
@pytest.mark.parametrize("kind", ["file", "symlink"])
def test_remove_stale_links(runner, yadm, tmpdir, kind, linked):
"""Test remove_stale_links()"""
source_file = tmpdir.join("source_file")
source_file.write("source file", ensure=True)
link = tmpdir.join("link")
if kind == "file":
link.write("link file", ensure=True)
else:
os.system(f"ln -s {source_file} {link}")
alt_linked = ""
if linked:
alt_linked = source_file
script = f"""
YADM_TEST=1 source {yadm}
possible_alt_targets=({link})
alt_linked=({alt_linked})
function rm() {{ echo rm "$@"; }}
remove_stale_links
"""
run = runner(command=["bash"], inp=script)
assert run.err == ""
if kind == "symlink" and not linked:
assert f"rm -f {link}" in run.out
else:
assert run.out == ""

View File

@ -39,13 +39,11 @@ CONDITION = {
TEMPLATE_LABELS = ["t", "template", "yadm"] TEMPLATE_LABELS = ["t", "template", "yadm"]
def calculate_score(filename): def calculate_score(conditions):
"""Calculate the expected score""" """Calculate the expected score"""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
score = 0 score = 0
_, conditions = filename.split("##", 1)
for condition in conditions.split(","): for condition in conditions.split(","):
label = condition label = condition
value = None value = None
@ -111,70 +109,70 @@ def test_score_values(runner, yadm, default, arch, system, distro, cla, host, us
local_distro = "testDISTro" local_distro = "testDISTro"
local_host = "testHost" local_host = "testHost"
local_user = "testUser" local_user = "testUser"
filenames = {"filename##": 0} conditions = {"": 0}
if default: if default:
for filename in list(filenames): for condition in list(conditions):
for label in CONDITION[default]["labels"]: for label in CONDITION[default]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += label newcond += label
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if arch: if arch:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[arch]["labels"]: for label in CONDITION[arch]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_arch if match else "badarch"]) newcond += ".".join([label, local_arch if match else "badarch"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if system: if system:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[system]["labels"]: for label in CONDITION[system]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_system if match else "badsys"]) newcond += ".".join([label, local_system if match else "badsys"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if distro: if distro:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[distro]["labels"]: for label in CONDITION[distro]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_distro if match else "baddistro"]) newcond += ".".join([label, local_distro if match else "baddistro"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if cla: if cla:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[cla]["labels"]: for label in CONDITION[cla]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_class if match else "badclass"]) newcond += ".".join([label, local_class if match else "badclass"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if host: if host:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[host]["labels"]: for label in CONDITION[host]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_host if match else "badhost"]) newcond += ".".join([label, local_host if match else "badhost"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
if user: if user:
for filename in list(filenames): for condition in list(conditions):
for match in [True, False]: for match in [True, False]:
for label in CONDITION[user]["labels"]: for label in CONDITION[user]["labels"]:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, local_user if match else "baduser"]) newcond += ".".join([label, local_user if match else "baduser"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
script = f""" script = f"""
YADM_TEST=1 source {yadm} YADM_TEST=1 source {yadm}
@ -187,15 +185,15 @@ def test_score_values(runner, yadm, default, arch, system, distro, cla, host, us
local_host={local_host} local_host={local_host}
local_user={local_user} local_user={local_user}
""" """
expected = "" expected = []
for filename, score in filenames.items(): for condition, score in conditions.items():
script += f""" script += f"""
score_file "{filename}" "dest" score_file "source" "target" "{condition}"
echo "{filename}" echo "{condition}=$score"
echo "$score"
""" """
expected += filename + "\n" expected.append(f"{condition}={score}")
expected += str(score) + "\n" expected.append("")
expected = "\n".join(expected)
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)
assert run.success assert run.success
assert run.err == "" assert run.err == ""
@ -206,15 +204,15 @@ def test_score_values(runner, yadm, default, arch, system, distro, cla, host, us
def test_extensions(runner, yadm, ext): def test_extensions(runner, yadm, ext):
"""Verify extensions do not effect scores""" """Verify extensions do not effect scores"""
local_user = "testuser" local_user = "testuser"
filename = f"filename##u.{local_user}" condition = f"u.{local_user}"
if ext: if ext:
filename += f",{ext}.xyz" condition += f",{ext}.xyz"
expected = "" expected = ""
script = f""" script = f"""
YADM_TEST=1 source {yadm} YADM_TEST=1 source {yadm}
score=0 score=0
local_user={local_user} local_user={local_user}
score_file "{filename}" score_file "source" "target" "{condition}"
echo "$score" echo "$score"
""" """
expected = f'{1000 + CONDITION["user"]["modifier"]}\n' expected = f'{1000 + CONDITION["user"]["modifier"]}\n'
@ -232,15 +230,15 @@ def test_score_values_templates(runner, yadm):
local_distro = "testdistro" local_distro = "testdistro"
local_host = "testhost" local_host = "testhost"
local_user = "testuser" local_user = "testuser"
filenames = {"filename##": 0} conditions = {"": 0}
for filename in list(filenames): for condition in list(conditions):
for label in TEMPLATE_LABELS: for label in TEMPLATE_LABELS:
newfile = filename newcond = condition
if not newfile.endswith("##"): if newcond:
newfile += "," newcond += ","
newfile += ".".join([label, "testtemplate"]) newcond += ".".join([label, "testtemplate"])
filenames[newfile] = calculate_score(newfile) conditions[newcond] = calculate_score(newcond)
script = f""" script = f"""
YADM_TEST=1 source {yadm} YADM_TEST=1 source {yadm}
@ -252,15 +250,15 @@ def test_score_values_templates(runner, yadm):
local_host={local_host} local_host={local_host}
local_user={local_user} local_user={local_user}
""" """
expected = "" expected = []
for filename, score in filenames.items(): for condition, score in conditions.items():
script += f""" script += f"""
score_file "{filename}" "dest" score_file "source" "target" "{condition}"
echo "{filename}" echo "{condition}=$score"
echo "$score"
""" """
expected += filename + "\n" expected.append(f"{condition}={score}")
expected += str(score) + "\n" expected.append("")
expected = "\n".join(expected)
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)
assert run.success assert run.success
assert run.err == "" assert run.err == ""
@ -281,7 +279,7 @@ def test_template_recording(runner, yadm, processor_generated):
YADM_TEST=1 source {yadm} YADM_TEST=1 source {yadm}
function record_score() {{ [ -n "$4" ] && echo "template recorded"; }} function record_score() {{ [ -n "$4" ] && echo "template recorded"; }}
{mock} {mock}
score_file "testfile##template.kind" score_file "source" "target" "template.kind"
""" """
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)
assert run.success assert run.success
@ -293,13 +291,13 @@ def test_underscores_and_upper_case_in_distro_and_family(runner, yadm):
"""Test replacing spaces with underscores and lowering case in distro / distro_family""" """Test replacing spaces with underscores and lowering case in distro / distro_family"""
local_distro = "test distro" local_distro = "test distro"
local_distro_family = "test family" local_distro_family = "test family"
filenames = { conditions = {
"filename##distro.Test Distro": 1004, "distro.Test Distro": 1004,
"filename##distro.test-distro": 0, "distro.test-distro": 0,
"filename##distro.test_distro": 1004, "distro.test_distro": 1004,
"filename##distro_family.test FAMILY": 1008, "distro_family.test FAMILY": 1008,
"filename##distro_family.test-family": 0, "distro_family.test-family": 0,
"filename##distro_family.test_family": 1008, "distro_family.test_family": 1008,
} }
script = f""" script = f"""
@ -308,15 +306,15 @@ def test_underscores_and_upper_case_in_distro_and_family(runner, yadm):
local_distro="{local_distro}" local_distro="{local_distro}"
local_distro_family="{local_distro_family}" local_distro_family="{local_distro_family}"
""" """
expected = "" expected = []
for filename, score in filenames.items(): for condition, score in conditions.items():
script += f""" script += f"""
score_file "{filename}" score_file "source" "target" "{condition}"
echo "{filename}" echo "{condition}=$score"
echo "$score"
""" """
expected += filename + "\n" expected.append(f"{condition}={score}")
expected += str(score) + "\n" expected.append("")
expected = "\n".join(expected)
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)
assert run.success assert run.success
assert run.err == "" assert run.err == ""
@ -332,17 +330,17 @@ def test_negative_class_condition(runner, yadm):
# 0 # 0
score=0 score=0
score_file "filename##~class.testclass" "dest" score_file "source" "target" "~class.testclass"
echo "score: $score" echo "score: $score"
# 16 # 16
score=0 score=0
score_file "filename##~class.badclass" "dest" score_file "source" "target" "~class.badclass"
echo "score2: $score" echo "score2: $score"
# 16 # 16
score=0 score=0
score_file "filename##~c.badclass" "dest" score_file "source" "target" "~c.badclass"
echo "score3: $score" echo "score3: $score"
""" """
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)
@ -363,27 +361,27 @@ def test_negative_combined_conditions(runner, yadm):
# (0) + (0) = 0 # (0) + (0) = 0
score=0 score=0
score_file "filename##~class.testclass,~distro.testdistro" "dest" score_file "source" "target" "~class.testclass,~distro.testdistro"
echo "score: $score" echo "score: $score"
# (1000 + 16) + (1000 + 4) = 2020 # (1000 + 16) + (1000 + 4) = 2020
score=0 score=0
score_file "filename##class.testclass,distro.testdistro" "dest" score_file "source" "target" "class.testclass,distro.testdistro"
echo "score2: $score" echo "score2: $score"
# 0 (negated class condition) # 0 (negated class condition)
score=0 score=0
score_file "filename##~class.badclass,~distro.testdistro" "dest" score_file "source" "target" "~class.badclass,~distro.testdistro"
echo "score3: $score" echo "score3: $score"
# (1000 + 16) + (4) = 1020 # (1000 + 16) + (4) = 1020
score=0 score=0
score_file "filename##class.testclass,~distro.baddistro" "dest" score_file "source" "target" "class.testclass,~distro.baddistro"
echo "score4: $score" echo "score4: $score"
# (1000 + 16) + (16) = 1032 # (1000 + 16) + (16) = 1032
score=0 score=0
score_file "filename##class.testclass,~class.badclass" "dest" score_file "source" "target" "class.testclass,~class.badclass"
echo "score5: $score" echo "score5: $score"
""" """
run = runner(command=["bash"], inp=script) run = runner(command=["bash"], inp=script)

View File

@ -61,12 +61,8 @@ def create_alt_files(
new_dir = basepath.join(ALT_DIR + suffix).join(CONTAINED) new_dir = basepath.join(ALT_DIR + suffix).join(CONTAINED)
new_dir.write(ALT_DIR + suffix, ensure=True) new_dir.write(ALT_DIR + suffix, ensure=True)
# Do not test directory support for jinja alternates test_paths = [new_file1, new_file2, new_dir]
test_paths = [new_file1, new_file2] test_names = [ALT_FILE1, ALT_FILE2, ALT_DIR]
test_names = [ALT_FILE1, ALT_FILE2]
if not re.match(r"##(t$|t\.|template|yadm)", suffix):
test_paths += [new_dir]
test_names += [ALT_DIR]
for test_path in test_paths: for test_path in test_paths:
if content: if content:

84
yadm
View File

@ -169,7 +169,7 @@ function main() {
function score_file() { function score_file() {
local source="$1" local source="$1"
local target="$2" local target="$2"
local conditions="${source#*##}" local conditions="$3"
score=0 score=0
local template_processor="" local template_processor=""
@ -223,7 +223,7 @@ function score_file() {
continue continue
;; ;;
t | template | yadm) t | template | yadm)
if [ -d "$source" ] || ((negate)); then if ((negate)); then
INVALID_ALT+=("$source") INVALID_ALT+=("$source")
else else
template_processor=$(choose_template_processor "$value") template_processor=$(choose_template_processor "$value")
@ -578,42 +578,50 @@ function alt() {
local alt_scores=() local alt_scores=()
local alt_template_processors=() local alt_template_processors=()
# For removing stale links local filename
local possible_alt_targets=() for filename in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do
local suffix="${filename#*##}"
local alt_source if [ "$filename" = "$suffix" ]; then
for alt_source in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do
local conditions="${alt_source#*##}"
if [ "$alt_source" = "$conditions" ]; then
continue continue
fi fi
local conditions="${suffix%%/*}"
suffix="${suffix:${#conditions}}"
local target_base="${alt_source%%##*}" local target="${YADM_BASE}/${filename%%##*}"
alt_source="${YADM_BASE}/${target_base}##${conditions%%/*}" if [ "${target#"$YADM_ALT/"}" != "$target" ]; then
local alt_target="${YADM_BASE}/${target_base}" target="${YADM_BASE}/${target#"$YADM_ALT/"}"
if [ "${alt_target#"$YADM_ALT/"}" != "$alt_target" ]; then
target_base="${alt_target#"$YADM_ALT/"}"
fi fi
alt_target="${YADM_BASE}/${target_base}" local source="${YADM_BASE}/${filename}"
if ! in_list "$alt_target" "${possible_alt_targets[@]}"; then # If conditions are given on a directory we check if this alt, without the
possible_alt_targets+=("$alt_target") # filename part, has a target that's a symlink pointing at this source
# (which was the legacy behavior for yadm) and if so remove this target.
if [ -n "$suffix" ]; then
if [ -L "$target" ] && [ "$target" -ef "${YADM_BASE}/${filename%"$suffix"}" ]; then
rm -f "$target"
fi
target="$target$suffix"
fi fi
score_file "$alt_source" "$alt_target" # Remove target if it's a symlink pointing at source
if [ -L "$target" ] && [ "$target" -ef "$source" ]; then
rm -f "$target"
fi
score_file "$source" "$target" "$conditions"
done done
local alt_linked=() local alt_linked=()
alt_linking alt_linking
remove_stale_links
report_invalid_alts report_invalid_alts
} }
function report_invalid_alts() { function report_invalid_alts() {
[ "$LEGACY_WARNING_ISSUED" = "1" ] && return [ "$LEGACY_WARNING_ISSUED" = "1" ] && return
[ "${#INVALID_ALT[@]}" = "0" ] && return [ "${#INVALID_ALT[@]}" = "0" ] && return
local path_list local path_list=""
local invalid
for invalid in "${INVALID_ALT[@]}"; do for invalid in "${INVALID_ALT[@]}"; do
path_list="$path_list * $invalid"$'\n' path_list="$path_list * $invalid"$'\n'
done done
@ -643,25 +651,6 @@ EOF
printf '%s\n' "$msg" >&2 printf '%s\n' "$msg" >&2
} }
function remove_stale_links() {
# review alternate candidates for stale links
# if a possible alt IS linked, but it's source is not part of alt_linked,
# remove it.
if readlink_available; then
for stale_candidate in "${possible_alt_targets[@]}"; do
if [ -L "$stale_candidate" ]; then
src=$(readlink "$stale_candidate" 2>/dev/null)
if [ -n "$src" ]; then
for review_link in "${alt_linked[@]}"; do
[ "$src" = "$review_link" ] && continue 2
done
rm -f "$stale_candidate"
fi
fi
done
fi
}
function set_local_alt_values() { function set_local_alt_values() {
local -a all_classes local -a all_classes
@ -716,20 +705,23 @@ function alt_linking() {
local source="${alt_sources[$index]}" local source="${alt_sources[$index]}"
local template_processor="${alt_template_processors[$index]}" local template_processor="${alt_template_processors[$index]}"
if [[ -L "$target" ]]; then if [ -L "$target" ]; then
rm -f "$target" rm -f "$target"
elif [[ -d "$target" ]]; then elif [ -d "$target" ]; then
echo "Skipping alt $source as $target is a directory" echo "Skipping alt $source as $target is a directory"
continue continue
else else
assert_parent "$target" assert_parent "$target"
fi fi
if [[ -n "$template_processor" ]]; then if [ -n "$template_processor" ]; then
template "$template_processor" "$source" "$target" template "$template_processor" "$source" "$target"
elif [[ "$do_copy" -eq 1 ]]; then elif [ "$do_copy" -eq 1 ]; then
$log "Copying $source to $target" $log "Copying $source to $target"
cp -f "$source" "$target" cp -f "$source" "$target"
elif [ -e "$target" ]; then
echo "Skipping alt $source as $target exists"
continue
else else
$log "Linking $source to $target" $log "Linking $source to $target"
ln_relative "$source" "$target" ln_relative "$source" "$target"
@ -748,7 +740,7 @@ function ln_relative() {
local rel_source local rel_source
rel_source=$(relative_path "$(builtin_dirname "$target")" "$source") rel_source=$(relative_path "$(builtin_dirname "$target")" "$source")
ln -fs "$rel_source" "$target" ln -s "$rel_source" "$target"
alt_linked+=("$rel_source") alt_linked+=("$rel_source")
} }
@ -2263,10 +2255,6 @@ function esh_available() {
command -v "$ESH_PROGRAM" &>/dev/null && return command -v "$ESH_PROGRAM" &>/dev/null && return
return 1 return 1
} }
function readlink_available() {
command -v "readlink" &>/dev/null && return
return 1
}
# ****** Directory translations ****** # ****** Directory translations ******

12
yadm.1
View File

@ -616,8 +616,16 @@ Subsystem for Linux, where the os is reported as WSL, the link will be:
If no "##default" version exists and no files have valid conditions, then no If no "##default" version exists and no files have valid conditions, then no
link will be created. link will be created.
Links are also created for directories named this way, as long as they have at Conditions can also be used on directories and will then apply on all files
least one yadm managed file within them. within the directory. The following files:
- $HOME/path/example.txt##os.Linux
- $HOME/path/subdir/other.txt##os.Linux
Would give the same result as:
- $HOME/path##os.Linux/example.txt
- $HOME/path##os.Linux/subdir/other.txt
yadm will automatically create these links by default. This can be disabled yadm will automatically create these links by default. This can be disabled
using the using the