Skip to content

Git dates never resolve when content/ is a symlink outside the repository #2

Description

@boxi-os

Repository.discover(ctx.argv.directory) follows the symlink, so the repository it finds is the one
containing the link target. The file path is then made relative to that repository's workdir, but
data.filePath is relative to the project directory, so path.relative() resolves it against
process.cwd() and produces a path that points back out of the repository. Every lookup throws, and
the build prints the "isn't yet tracked by git" warning for every page that has no modified in its
frontmatter.

This only happens when content/ points outside the repository — a common setup for an Obsidian
vault kept in its own git repository. In an ordinary project content/ is inside the repo and the
arithmetic works out.

Reproduce

mkdir -p /tmp/vault && cd /tmp/vault && git init -q
printf -- '---\ntitle: A\n---\nhi\n' > a.md && git add -A && git commit -qm a

cd /path/to/a/quartz/project
rm -rf content && ln -s /tmp/vault content
npx quartz build
Warning: content/a.md isn't yet tracked by git, dates will be inaccurate

Measured

Version 0.1.0, @napi-rs/simple-git 0.1.22, node 26.5.1, macOS. Project at
~/Documents/Example, content → ~/Obsidian/QuartzProjekte/Example (its own git repository,
clean, 286 files tracked).

const repo = Repository.discover('content')
repo.workdir()        // '/Users/boxi/Obsidian/QuartzProjekte/Example/'  <- the vault, as expected
path.relative(repo.workdir(), 'content/en/formatting/special/escapes.md')
// '../../../Documents/Example/content/en/formatting/special/escapes.md'  <- leads back out
await repo.getFileLatestModifiedDateAsync(that)
// throws: Failed to get commit for [...]
await repo.getFileLatestModifiedDateAsync('en/formatting/special/escapes.md')
// 1788573656000  <- the repository does know the file

250 of 254 pages warned. The four silent ones carry lastmod in their frontmatter, so modified ||=
short-circuits and git is never asked.

Suggested fix

Resolve the file path the same way discover() resolved the directory, so both sides of
path.relative live in the same tree:

-const relativePath = path.relative(repositoryWorkdir, fullFp)
+const relativePath = path.relative(repositoryWorkdir, fs.realpathSync(fullFp))

Measured with that one change applied to dist/index.js: the build finishes with 0 warnings, and
the rendered timestamp changes from 2026-09-05T03:41:43.937Z (milliseconds — a filesystem mtime,
the third priority silently taking over) to 2026-09-05T03:43:04.000Z (whole seconds — the commit).
fs is already imported. Behaviour for a non-symlinked content/ is unchanged, since realpathSync
is then a no-op apart from normalisation.

Two things worth deciding alongside it, both beyond what I measured:

  • realpathSync throws on a broken link, which would turn a dangling symlink into a hard failure
    rather than a warning; wrapping it, or falling back to fullFp, keeps the current shape.
  • The warning currently fires for every page on every build, which is 250 identical lines here. It
    might read better as one line naming the count, or once per repository.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions