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 | "\n", | ||
| 410 | "docs/guide.md": "# Guide\n\n[back](../README.md) \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 | } | ||