Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Resolve a README's relative links against the file, not the page

796ccd9051ef49f10655088ec39b8dfb844f4124 · Keenan Nemetz · 13d ago · parent 53ff967b

Resolve a README's relative links against the file, not the page

A relative link was resolved by the browser against the URL the README happened
to be rendered at, so `docs/getting-started.md` reached
`/r/<repo>/docs/getting-started.md` from the overview page and 404'd, and the
same link broke differently from the code browser.

A document that lives in the tree now carries its own path, and its links and
images are rewritten after parsing and before rendering: markdown opens the
preview, other files open the source, images point at the raw route, a
directory opens the tree, and `.`/`..` resolve the way a reader expects with
climbing clamped at the repository root. A destination that already names
somewhere — a scheme, `//host`, a site-absolute path, a bare `#anchor` or
`?query` — is left alone, and so is any document with no place in a tree, such
as an issue body or a comment.

The rewrite works on the parsed tree rather than on the markdown text, so
reference-style links are covered too, and the source view still shows the
file's bytes unchanged.

Refs #01M5XF52R132A88C6WEQ6H6BK6

server/web_test.go +65

@@ -392,3 +392,68 @@ func TestReadmeIsSanitised(t *testing.T) {
392 392 t.Fatalf("rendered README executed a tag:\n%s", body)
393 393 }
394 394 }
395
396 // TestReadmeRelativeLinksOpenTheFileTheyName is the bug this fixes: a relative
397 // link in a README resolves against the page it is rendered on, so `/r/demo` sent
398 // `docs/guide.md` to `/r/demo/docs/guide.md`. It has to resolve against the
399 // document instead, on both surfaces that render the README, and a document in a
400 // subdirectory has to resolve its own links the same way.
401 func TestReadmeRelativeLinksOpenTheFileTheyName(t *testing.T) {
402 t.Parallel()
403 f := newFixture(t)
404 f.publish(f.repo)
405 work := workRepo(t, map[string]string{
406 "README.md": "# Demo\n\n" +
407 "[guide](docs/guide.md) [terms](LICENSE) [workflow](.vole/workflows/verify.yaml)\n\n" +
408 "[site](https://example.com/x) [top](#demo) [dir](docs/)\n\n" +
409 "![shot](img/shot.png)\n",
410 "docs/guide.md": "# Guide\n\n[back](../README.md) ![local](shot.png)\n",
411 "LICENSE": "terms\n",
412 "img/shot.png": "not really a png\n",
413 })
414 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
415 t.Fatalf("push: %s", errs)
416 }
417
418 want := []string{
419 `href="/r/demo/blob/main/docs/guide.md?preview=1"`,
420 `href="/r/demo/blob/main/LICENSE"`,
421 `href="/r/demo/blob/main/.vole/workflows/verify.yaml"`,
422 `href="/r/demo/tree/main/docs"`,
423 `href="https://example.com/x"`,
424 `href="#demo"`,
425 `src="/r/demo/raw/main/img/shot.png"`,
426 }
427 for _, page := range []string{"/r/" + f.repo, "/r/" + f.repo + "/code"} {
428 code, body := f.get(page)
429 if code != 200 {
430 t.Fatalf("%s = %d", page, code)
431 }
432 for _, w := range want {
433 if !strings.Contains(body, w) {
434 t.Fatalf("%s does not contain %s:\n%s", page, w, body)
435 }
436 }
437 }
438
439 // A document below the root resolves against its own directory.
440 code, body := f.get("/r/" + f.repo + "/blob/main/docs/guide.md?preview=1")
441 if code != 200 {
442 t.Fatalf("guide preview = %d", code)
443 }
444 for _, w := range []string{
445 `href="/r/demo/blob/main/README.md?preview=1"`,
446 `src="/r/demo/raw/main/docs/shot.png"`,
447 } {
448 if !strings.Contains(body, w) {
449 t.Fatalf("guide preview does not contain %s:\n%s", w, body)
450 }
451 }
452
453 // The source view shows the file's bytes, so the link stays as it was written
454 // there rather than becoming the URL the preview would use.
455 if _, body := f.get("/r/" + f.repo + "/blob/main/README.md"); !strings.Contains(body, "docs/guide.md") ||
456 strings.Contains(body, `href="/r/demo/blob/main/docs/guide.md?preview=1"`) {
457 t.Fatalf("the source view is not the file as written:\n%s", body)
458 }
459 }