Commit7dfd53a3Recorded23 Sep 2026Repositorydocket

Render: a tracked symlink to a directory is named, not read

Message

git ls-tree lists a tracked symlink as one leaf (mode 120000). When it points at a directory, file-verdict read it as a file, raised `read-string: Is a directory', and aborted the WHOLE project render. Found 2026-09-23 rendering the real estate for the code.usesigil.org move: four Sigil repos track such links (sigil's test corpus links; .sigil/deps/* in sigil-rss, sigil-twitch, sigil-web-styles), and each one alone killed the Sigil project's render.

The link now gets no page and is NAMED with the other exclusions ("a symbolic link to a directory; git tracks the link, not its contents"). `directory?' follows links (probed: link-to-dir #t, real dir #t, link-to-file #f), so a link to a FILE still renders its target.

test-render.sh builds a repo tracking both kinds, with the file target outside the repo so only the link can put its text on the site. It asserts the render succeeds, the directory link is named as excluded, and the file link's content is rendered. green: rc=0, render gate GREEN red (clause disabled, hash-guarded plant, rebuilt): rc=1, "a tracked symlink to a directory aborted the render", the original read-string error; restored to the pre-plant hash and rebuilt.

Changed
 src/docket/render.sgl           | 10 ++++++++++
 test/integration/test-render.sh | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 44 insertions(+)
Diff
src/docket/render.sglmodified
@@ -1532,6 +1532,16 @@
1532
(cons "the path contains # or ?, which cannot appear unescaped in a URL" #f))
1533
((not (file-exists? (path-join dir rel)))
1534
(cons "no such file on disk at tip" #f))
+1535
;; A TRACKED SYMLINK TO A DIRECTORY. git ls-tree lists the link as
+1536
;; one leaf (mode 120000); on disk it resolves to a directory, and
+1537
;; reading it as a file raised `read-string: Is a directory', which
+1538
;; aborted the WHOLE render. Found 2026-09-23 rendering the real
+1539
;; estate: sigil (test corpus links), sigil-rss, sigil-twitch and
+1540
;; sigil-web-styles (tracked .sigil/deps links) each killed the
+1541
;; Sigil project's render on its own. Named, not rendered: git
+1542
;; tracks the link, not what it points at.
+1543
((directory? (path-join dir rel))
+1544
(cons "a symbolic link to a directory; git tracks the link, not its contents" #f))
1545
(else
1546
(let ((text (read-file-string (path-join dir rel))))
1547
(if (binary-string? text) (cons "binary" #f) (cons #f text))))))
test/integration/test-render.shmodified
@@ -1964,6 +1964,40 @@ rm -rf "$tmp/site-dormant"
1964
--project "$repo/test/fixtures/chronicle/project.sgl" --repos "$tmp/repos-dormant.json" \
1965
--output "$tmp/site-dormant" --report-url "http://127.0.0.1:8765/" \
1966
--site-url 'https://example.test' >/dev/null
+1967
# A TRACKED SYMLINK TO A DIRECTORY MUST NOT KILL THE RENDER. Found 2026-09-23
+1968
# on the real estate: four Sigil repos track such links (.sigil/deps/*, test
+1969
# corpus links), and each one alone aborted the project's render with
+1970
# `read-string: Is a directory'. The link gets no page and is NAMED, like every
+1971
# other exclusion. The link to a FILE is the positive control: it still renders.
+1972
mkdir -p "$tmp/repos/linky/realdir"
+1973
cp "$repo/test/fixtures/chronicle/sigil-web/package.sgl" "$tmp/repos/linky/package.sgl"
+1974
printf 'inside the linked directory\n' >"$tmp/repos/linky/realdir/inner.txt"
+1975
# The file target lives OUTSIDE the repo, so its text can reach the site only
+1976
# through the link -- otherwise target.txt's own page would satisfy the
+1977
# positive control below whether or not the link rendered.
+1978
printf 'the linked file\n' >"$tmp/linky-target.txt"
+1979
ln -s realdir "$tmp/repos/linky/linkdir"
+1980
ln -s "$tmp/linky-target.txt" "$tmp/repos/linky/linkfile.txt"
+1981
git -C "$tmp/repos/linky" init -q
+1982
git -C "$tmp/repos/linky" -c user.name='Docket TEST-ONLY' -c user.email=test-only@example.invalid add .
+1983
git -C "$tmp/repos/linky" -c user.name='Docket TEST-ONLY' -c user.email=test-only@example.invalid \
+1984
commit -q -m 'TEST-ONLY tracked symlinks'
+1985
[[ $(git -C "$tmp/repos/linky" ls-files -s linkdir | cut -d' ' -f1) == 120000 ]] || {
+1986
echo 'FAIL: fixture setup -- linkdir is not tracked as a symlink, so this test proves nothing' >&2; exit 1; }
+1987
printf '[{"name":"linky","path":"%s","package":"%s"}]\n' \
+1988
"$tmp/repos/linky" "$tmp/repos/linky/package.sgl" >"$tmp/repos-linky.json"
+1989
rm -rf "$tmp/site-linky"
+1990
linky_out=$("$repo/docket" render --record "$tmp/emptyrec" --state "$tmp/emptystate" \
+1991
--project "$repo/test/fixtures/chronicle/project.sgl" --repos "$tmp/repos-linky.json" \
+1992
--output "$tmp/site-linky" --report-url "http://127.0.0.1:8765/" 2>&1) || {
+1993
printf '%s\n' "$linky_out" >&2
+1994
echo 'FAIL: a tracked symlink to a directory aborted the render' >&2; exit 1; }
+1995
grep -Fq 'excluded linkdir (a symbolic link to a directory' <<<"$linky_out" || {
+1996
printf '%s\n' "$linky_out" >&2
+1997
echo 'FAIL: the symlinked directory was not NAMED as excluded' >&2; exit 1; }
+1998
grep -rlFq 'the linked file' "$tmp/site-linky/sigil/repos/linky/" || {
+1999
echo 'FAIL: positive control -- the symlink to a FILE no longer renders its content' >&2; exit 1; }
+2000
2001
# --site-url IS REQUIRED HERE, and not for realism. Without it NO feed is ever
2002
# written, so every feed assertion below -- that one exists, that it is dated
2003
# from the record, that the page links it -- would be asking about a render