Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resolution

Parsing turns each document into a list of occurrences: inline links and images, reference style links, and autolinks. Each occurrence keeps two spellings of its destination. The raw one is the exact bytes from the source. The semantic one is what those bytes mean after the format’s own decoding. So [a](&b) records both &b and &b, and a change to either the spelling or the meaning is visible later.

What the parser cannot see into is declared instead of skipped. Raw HTML blocks and MDX expressions become opaque regions, reported with their size and place as opaque-html-region and opaque-mdx-region findings, so a link hidden inside JSX is a stated blind spot rather than an invisible one.

Each destination then resolves against the tree, and only three shapes are in scope. A relative path resolves from the document’s own directory and must stay inside the repository; ../../etc/passwd is an invalid-reference, not a file read. A repository-rooted path resolves from the root. And when the invocation provides the --repository triple and a dialect, a URL on the declared host that names this same repository in the dialect’s own spelling and a ref the scan can vouch for is converted to the path it points at. Every other URL is external-out-of-scope: counted, reported, left alone.

Three dialects exist, each pinned to the exact URL grammar its forge’s browser emits. The github dialect reads owner/name/blob-or-tree/ref/path and serves GitHub and any GitHub Enterprise host the identity declares. The gitlab dialect reads the canonical separator form group[/subgroup...]/name/-/blob-or-tree/ref/path, nested groups compared whole. The gitea dialect serves Gitea, Forgejo, and Codeberg with typed selectors: src/branch/ splits like the others, src/commit/ resolves exactly when its full lowercase object id is the candidate commit and is out of version scope otherwise, and src/tag/ is always out of version scope because no tag is a trusted ref. Line anchors follow the forge: #L10-L20 is a line reference on github and gitea, #L10-20 on gitlab, and each spelling is nothing on the other’s forge. A recognized reference’s intent kind names the dialect that read it, not the host, so an Enterprise repository’s links carry the same kind GitHub’s do.

One document, every destination shape:

[guide](guide.md)                     resolves beside this document
[root](/docs/guide.md)                resolves from the repository root
[escape](../../etc/passwd)            invalid-reference: it leaves the repository
[dir](sub/)                           the author promised a directory
[gh](https://github.com/o/r/blob/main/src/lib.rs)   a path, when the triple names o/r on github.com
[web](https://example.com/manual)     external-out-of-scope: counted, left alone
[anchor](guide.md#setup)              the path resolves; the fragment is recorded, not checked

The same decision, drawn:

resolvedestdestinationrelrelative orrooted pathdest->relghforge URL,same repositorydest->ghwith the tripleotherany other URLdest->othertreeresolve againstthe treerel->treegh->treeextexternal-out-of-scopeother->exthittarget bytesand mode readtree->hitfoundmissexplicit-target-missingtree->missabsent

Resolution is exact, and the small rules matter. A trailing slash means the author promised a directory, so sub/ must be a tree and guide.md/ is a type mismatch even though guide.md exists. Percent-encoding is decoded exactly once, so %252F stays as the literal three characters %2F in the name instead of turning into a second slash. A percent escape may decode to bytes that are not text at all, and those bytes are simply the path: bad-%FF-name.md resolves against the tree entry carrying that exact byte, because Git names files in bytes and so does the resolver. Query strings and fragments are recorded as digests but ignored for resolution, because a tree has no queries and no anchors. One narrow divergence is deliberate: a fragment whose escapes decode outside UTF-8 is dropped rather than digested, since carrying it would change the recorded identity of every existing observation for no resolution gain. Heading anchors, site routes, code symbols, and version-pinned references are all reported as unsupported-reference-semantics: real checks for those belong to tools that have the right information, and a guess here would turn honest ignorance into a false pass.

Each resolved target is read from the object store and hashed, so the comparison knows the exact bytes and file mode on both sides. A symlink or submodule target is unsupported-target-kind, because following one leaves the world of exact bytes where the guarantees live. A Git LFS pointer file is recognized and its declared content digest is carried, so swapping the large file behind a pointer counts as a change even though the pointer text barely moves.