A wiki can start with a push 6e7dfa0a42b9
When someone who may write to a repository reaches OWNER/NAME.wiki before the wiki exists, the hub makes it, empty and unbound, so the first pages can be pushed as well as written on the web; the push gives it its id. Readers still find nothing there. Test a_wiki_can_start_with_a_push.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-05 12:12 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
6e7dfa0a42b996d4521e15039a06833f4efe8073bc815ee731b6233b2cf8aefa- Depends on
- 145306044896 66fd9d6b1452 749d9a5fcd9f 8475176972e8 b5668108487f d23464c0ac60
- Depended on by
- c8684d44d549 ffb67a81188c 554ccacfc123
- 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.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
- 3 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified CHANGELOG.md +2 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **A wiki can start with a push.** Someone who may write to a repository can push the first pages of its wiki to `OWNER/NAME.wiki`, as well as writing them on the web: the hub makes the empty wiki when a writer first reaches that address, and the push gives it its id. Readers still find nothing there until it has pages. |
|
| 8 | + | ||
| 7 | 9 | - **Mermaid diagrams in Markdown.** A ```` ```mermaid ```` block in a README, wiki page, issue or proposal is drawn as a diagram, in the page's light or dark theme. The hub serves its own copy of Mermaid 12.1.0, loads it only on pages that have a diagram, and runs it in strict mode. A diagram that does not parse stays as its source, with the reason under it. |
|
| 8 | 10 | ||
| 9 | 11 | - **An organisation's storage policy (#96).** An organisation's Settings › Storage can require every new repository to live in a bucket and list the buckets allowed, so its data stays where its own buckets are. The New repository form refuses anything else and says what is allowed. |
|
| … | 125 unchanged lines |
modified crates/hub/src/protocol.rs +18 −0
| … | 164 unchanged lines |
||
| 165 | 165 | if !auth::roles::allows(role, Access::Read) { |
|
| 166 | 166 | return Err(not_found()); |
|
| 167 | 167 | } |
|
| 168 | + | // A wiki is made, empty and unbound, when someone who may write to |
|
| 169 | + | // its repository first reaches it, so the first page can be pushed |
|
| 170 | + | // as well as written on the web (§6.11); the push gives it its id. |
|
| 171 | + | let (_, is_wiki) = crate::wiki::parent_of(&parsed); |
|
| 172 | + | let may_write = match &caller { |
|
| 173 | + | Caller::Site => true, |
|
| 174 | + | Caller::Token(_, token) => token.has_scope("repo:write") || token.has_scope("repo:admin"), |
|
| 175 | + | _ => false, |
|
| 176 | + | }; |
|
| 177 | + | if is_wiki && may_write && auth::roles::allows(role, Access::Write) && !row.archived { |
|
| 178 | + | if let Some(name) = RepoName::new(&row.owner, &row.name) { |
|
| 179 | + | if !self.wikis.exists(&name) { |
|
| 180 | + | self.wikis |
|
| 181 | + | .create(&name, None) |
|
| 182 | + | .map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, format!("make the wiki: {e}")))?; |
|
| 183 | + | } |
|
| 184 | + | } |
|
| 185 | + | } |
|
| 168 | 186 | match access { |
|
| 169 | 187 | oxy_server::Access::Read if scope_ok => Ok(()), |
|
| 170 | 188 | oxy_server::Access::Read => Err(Failure(StatusCode::FORBIDDEN, "this token cannot read repositories".into())), |
|
| … | 184 unchanged lines |
modified crates/hub/tests/edits.rs +47 −0
| … | 791 unchanged lines |
||
| 792 | 792 | assert!(r.body.contains("The wiki is in the trash"), "{}", r.body); |
|
| 793 | 793 | assert!(send(app, "GET", "/andy/demo/wiki", Some(&andy), None).await.body.contains("No wiki yet")); |
|
| 794 | 794 | } |
|
| 795 | + | ||
| 796 | + | /// A wiki can start with a push: someone who may write to the repository |
|
| 797 | + | /// reaching `OWNER/NAME.wiki` before it exists gets an empty wiki, which |
|
| 798 | + | /// the push fills; a reader still finds nothing there. |
|
| 799 | + | #[tokio::test(flavor = "multi_thread")] |
|
| 800 | + | async fn a_wiki_can_start_with_a_push() { |
|
| 801 | + | hermetic(); |
|
| 802 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 803 | + | let mut config = Config::load(dir.path()).unwrap(); |
|
| 804 | + | config.auth.push_tokens = vec!["site".into()]; |
|
| 805 | + | let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
|
| 806 | + | let port = listener.local_addr().unwrap().port(); |
|
| 807 | + | config.server.base_url = format!("http://127.0.0.1:{port}"); |
|
| 808 | + | let app = oxygenhub::build(config).await.unwrap(); |
|
| 809 | + | let router = oxygenhub::router(app.clone()); |
|
| 810 | + | tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); |
|
| 811 | + | let base = format!("http://127.0.0.1:{port}"); |
|
| 812 | + | let repo = RepoName::parse("andy/docs").unwrap(); |
|
| 813 | + | app.repos.create(&repo, None).unwrap(); |
|
| 814 | + | db::repos::create(&app.db, "andy", "docs", "", hub_api::Visibility::Public).await.unwrap(); |
|
| 815 | + | let url = format!("{base}/andy/docs.wiki"); |
|
| 816 | + | ||
| 817 | + | // A reader: nothing there, and nothing made. |
|
| 818 | + | let anon = tempfile::tempdir().unwrap(); |
|
| 819 | + | let u = url.clone(); |
|
| 820 | + | let p = anon.path().join("w"); |
|
| 821 | + | assert!(tokio::task::spawn_blocking(move || Repo::clone(&u, &p)).await.unwrap().is_err()); |
|
| 822 | + | assert!(!app.wikis.exists(&repo)); |
|
| 823 | + | ||
| 824 | + | // A writer pushes pages it wrote locally. |
|
| 825 | + | let work = tempfile::tempdir().unwrap(); |
|
| 826 | + | let mut w = Repo::init(work.path()).unwrap(); |
|
| 827 | + | w.config.set("remote.origin.url", &url).unwrap(); |
|
| 828 | + | w.config.set("remote.origin.token", "site").unwrap(); |
|
| 829 | + | w.save_config().unwrap(); |
|
| 830 | + | write(work.path(), "Home.md", b"# Docs\n\nSee [the overview](Overview).\n"); |
|
| 831 | + | write(work.path(), "Overview.md", b"# Overview\n\n```mermaid\nflowchart LR\n A --> B\n```\n"); |
|
| 832 | + | w.snapshot("t").unwrap(); |
|
| 833 | + | w.commit("First pages", &[]).unwrap(); |
|
| 834 | + | let pushed = tokio::task::spawn_blocking(move || w.push("origin", false).unwrap()).await.unwrap(); |
|
| 835 | + | assert!(pushed.iter().all(|r| r.ok), "{pushed:?}"); |
|
| 836 | + | assert!(app.wikis.exists(&repo)); |
|
| 837 | + | let home = send(&app, "GET", "/andy/docs/wiki", None, None).await.body; |
|
| 838 | + | assert!(home.contains("<h1>Docs</h1>") && home.contains("href=\"/andy/docs/wiki/Overview\""), "{home}"); |
|
| 839 | + | let overview = send(&app, "GET", "/andy/docs/wiki/Overview", None, None).await.body; |
|
| 840 | + | assert!(overview.contains("class=\"language-mermaid\""), "a diagram block for the page's script to draw: {overview}"); |
|
| 841 | + | } |