Issue, comment and proposal Markdown resolves relative links against the default bookmark, release notes against the release's tag c79a1111dd56
closes #6
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-28 15:08 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
c79a1111dd56a694829779446d7f0771d1a803390ac02618e0f01f511d139e1d- Depends on
- 18e381ea88c2 3f4b0ea6eabf 736f6813543d 78001759604c 93be9ce4653e de505df5cf8d e1ec9fbf538e
- Depended on by
- 046e3352cdfd 439ecd8708a1 6a306cd03fc1 657e7d5c9da9 3b1bc27fe805 ffb67a81188c acc85ac0f70b b1e92a6233ee 10708ab8f54d
- In bookmarks
- api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
- In tags
- v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 11 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified crates/hub/src/web/issues.rs +10 −8
| … | 13 unchanged lines |
||
| 14 | 14 | use serde::Deserialize; |
|
| 15 | 15 | use std::collections::HashMap; |
|
| 16 | 16 | ||
| 17 | - | /// Markdown with cross references linked, for a repository. |
|
| 18 | - | pub async fn markdown(app: &Shared, repo_id: i64, repo_url: &str, text: &str) -> String { |
|
| 19 | - | let (issues, proposals) = idb::numbers(&app.db, repo_id).await.unwrap_or_default(); |
|
| 20 | - | let linked = render::refs::link(text, &Numbers { repo_url, issues: &issues, proposals: &proposals }); |
|
| 21 | - | render::markdown::render(&linked, &format!("{repo_url}/file/main/"), &format!("{repo_url}/raw/main/")) |
|
| 17 | + | /// Markdown with cross references linked, for a repository; relative |
|
| 18 | + | /// links and images resolve against `reference` (the default bookmark for |
|
| 19 | + | /// issues and proposals, the tag for release notes). |
|
| 20 | + | pub async fn markdown(app: &Shared, row: &db::repos::RepoRow, reference: &str, text: &str) -> String { |
|
| 21 | + | let repo_url = format!("/{}", row.slug()); |
|
| 22 | + | let (issues, proposals) = idb::numbers(&app.db, row.id).await.unwrap_or_default(); |
|
| 23 | + | let linked = render::refs::link(text, &Numbers { repo_url: &repo_url, issues: &issues, proposals: &proposals }); |
|
| 24 | + | render::markdown::render(&linked, &format!("{repo_url}/file/{reference}/"), &format!("{repo_url}/raw/{reference}/")) |
|
| 22 | 25 | } |
|
| 23 | 26 | ||
| 24 | 27 | /// Re-index a thread for search: title, body and every comment. |
|
| … | 298 unchanged lines |
||
| 323 | 326 | let (_, row, role) = lookup(app, &shell, viewer, owner, name).await?; |
|
| 324 | 327 | let e = |e| Error::from_db(shell.clone(), e); |
|
| 325 | 328 | let i = idb::get(&app.db, row.id, number).await.map_err(e)?.ok_or_else(|| Error::not_found(shell.clone()))?; |
|
| 326 | - | let repo_url = format!("/{owner}/{name}"); |
|
| 327 | - | let body_html = markdown(app, row.id, &repo_url, &i.body).await; |
|
| 329 | + | let body_html = markdown(app, &row, &row.default_bookmark, &i.body).await; |
|
| 328 | 330 | let comments = idb::comments(&app.db, i.id).await.map_err(e)?; |
|
| 329 | 331 | let events = idb::events(&app.db, i.id).await.map_err(e)?; |
|
| 330 | 332 | let mut timeline: Vec<(i64, Item)> = Vec::new(); |
|
| 331 | 333 | for c in &comments { |
|
| 332 | - | timeline.push((c.created_ms, Item::Comment(c.clone(), markdown(app, row.id, &repo_url, &c.body).await))); |
|
| 334 | + | timeline.push((c.created_ms, Item::Comment(c.clone(), markdown(app, &row, &row.default_bookmark, &c.body).await))); |
|
| 333 | 335 | } |
|
| 334 | 336 | for ev in events { |
|
| 335 | 337 | if ev.event != "opened" { |
|
| … | 643 unchanged lines |
modified crates/hub/src/web/proposals.rs +1 −1
| … | 670 unchanged lines |
||
| 671 | 671 | projects, |
|
| 672 | 672 | shell, |
|
| 673 | 673 | repo: RepoShell::new(app, &row, "proposals").with_role(role), |
|
| 674 | - | body_html: super::issues::markdown(app, row.id, &format!("/{owner}/{name}"), &p.body).await, |
|
| 674 | + | body_html: super::issues::markdown(app, &row, &row.default_bookmark, &p.body).await, |
|
| 675 | 675 | p, |
|
| 676 | 676 | tab, |
|
| 677 | 677 | changes, |
|
| … | 495 unchanged lines |
modified crates/hub/src/web/releases.rs +1 −1
| … | 37 unchanged lines |
||
| 38 | 38 | for r in list { |
|
| 39 | 39 | let assets = rdb::assets(&app.db, r.id).await?; |
|
| 40 | 40 | let (heads, signer) = tags.get(&r.tag).cloned().unwrap_or_default(); |
|
| 41 | - | let notes_html = super::issues::markdown(app, row.id, &format!("/{}", row.slug()), &r.notes).await; |
|
| 41 | + | let notes_html = super::issues::markdown(app, row, &r.tag, &r.notes).await; |
|
| 42 | 42 | out.push(ReleaseView { latest: Some(r.id) == latest, r, notes_html, assets, heads, signer }); |
|
| 43 | 43 | } |
|
| 44 | 44 | Ok(out) |
|
| … | 447 unchanged lines |
modified crates/hub/tests/issues.rs +26 −0
| … | 802 unchanged lines |
||
| 803 | 803 | assert_eq!(get(app, "/andy/demo/issue/1", None).await.status, StatusCode::OK); |
|
| 804 | 804 | assert_eq!(get(app, "/projects/nobody", None).await.status, StatusCode::NOT_FOUND); |
|
| 805 | 805 | } |
|
| 806 | + | ||
| 807 | + | #[tokio::test(flavor = "multi_thread")] |
|
| 808 | + | async fn issue_markdown_follows_the_default_bookmark() { |
|
| 809 | + | let h = hub().await; |
|
| 810 | + | let app = &h.app; |
|
| 811 | + | register(app, "andy").await; |
|
| 812 | + | app.repos.create(&RepoName::parse("andy/trunky").unwrap(), None).unwrap(); |
|
| 813 | + | let row = db::repos::create(&app.db, "andy", "trunky", "", hub_api::Visibility::Public).await.unwrap(); |
|
| 814 | + | sqlx::query("UPDATE repositories SET default_bookmark = 'trunk' WHERE id = ?").bind(row.id).execute(&app.db).await.unwrap(); |
|
| 815 | + | db::issues::create( |
|
| 816 | + | &app.db, |
|
| 817 | + | db::issues::NewIssue { |
|
| 818 | + | repo_id: row.id, |
|
| 819 | + | title: "Docs", |
|
| 820 | + | body: "See [the guide](docs/guide.md) and .", |
|
| 821 | + | author_id: None, |
|
| 822 | + | author: "andy", |
|
| 823 | + | }, |
|
| 824 | + | ) |
|
| 825 | + | .await |
|
| 826 | + | .unwrap(); |
|
| 827 | + | let page = get(app, "/andy/trunky/issue/1", None).await.body; |
|
| 828 | + | assert!(page.contains("href=\"/andy/trunky/file/trunk/docs/guide.md\""), "{page}"); |
|
| 829 | + | assert!(page.contains("src=\"/andy/trunky/raw/trunk/img/a.png\"")); |
|
| 830 | + | assert!(!page.contains("/file/main/")); |
|
| 831 | + | } |
modified docs/spec.md +1 −1
| … | 369 unchanged lines |
||
| 370 | 370 | ||
| 371 | 371 | Per-repository numbered issues sharing the number space with proposals (as GitHub does), with title, Markdown body, author, assignees, labels (name, colour, description), a milestone (title, due date, description, open/closed), state (`open`, `closed` with reason `completed` or `not planned`), comments, lock, and pin. Issue templates come from `.oxyhub/ISSUE_TEMPLATE/*.md` in the default state. |
|
| 372 | 372 | ||
| 373 | - | Cross references: `#12` links an issue or proposal; `owner/name#12` across repositories; a 12-hex-or-longer token links a change when it resolves; `closes #12` in a change message closes the issue when the change lands on the default bookmark ("closed by change"). Any other `#12` in a pushed change's message, on any bookmark, puts "referenced by change <hash> on <bookmark>" on that issue's or proposal's timeline, the hash linking to the change; other repositories' `owner/name#12` are left alone. |
|
| 373 | + | Relative links and images in issue, comment and proposal text resolve against the repository's default bookmark, and in release notes against the release's tag. Cross references: `#12` links an issue or proposal; `owner/name#12` across repositories; a 12-hex-or-longer token links a change when it resolves; `closes #12` in a change message closes the issue when the change lands on the default bookmark ("closed by change"). Any other `#12` in a pushed change's message, on any bookmark, puts "referenced by change <hash> on <bookmark>" on that issue's or proposal's timeline, the hash linking to the change; other repositories' `owner/name#12` are left alone. |
|
| 374 | 374 | ||
| 375 | 375 | Import from GitHub or GitLab is a 1.x item. |
|
| 376 | 376 | ||
| … | 207 unchanged lines |