Resolve in VS Code; the name conflict test picks its candidate reliably 16cb2b3aa947
Both resolver pages link the conflict to oxygen-vscode (vscode://oxygen.oxygen-vscode/resolve?url=…&bookmark=…&path=…, encoded), which opens the checkout on the bookmark and the file. The name conflict test chose which candidate held "second" from the first occurrence of the words anywhere on the page, so it failed now and then; it now reads the candidates' previews. closes #32
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-29 05:10 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
16cb2b3aa9479bdc9a3cdd2c322f8d437983321c67c655d8d4f4337ae05dcf60- Depends on
- 145306044896 93be9ce4653e c5f3bde466d1 fd958fbc06fb
- Depended on by
- c96b32865000 129c23effb73 d3e75fb41e61 9e5ef6c23006
- 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
- 12 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified crates/hub/src/web/resolve.rs +12 −0
| … | 17 unchanged lines |
||
| 18 | 18 | use oxy_core::repo::FileEdit; |
|
| 19 | 19 | use std::collections::HashMap; |
|
| 20 | 20 | ||
| 21 | + | /// "Resolve in VS Code": the link oxygen-vscode handles (its README). |
|
| 22 | + | pub fn vscode_link(app: &Shared, owner: &str, name: &str, bookmark: &str, path: &str) -> String { |
|
| 23 | + | use super::settings::urlencode; |
|
| 24 | + | let url = format!("{}/{owner}/{name}", app.config.base_url().trim_end_matches('/')); |
|
| 25 | + | format!("vscode://oxygen.oxygen-vscode/resolve?url={}&bookmark={}&path={}", urlencode(&url), urlencode(bookmark), urlencode(path)) |
|
| 26 | + | } |
|
| 27 | + | ||
| 21 | 28 | /// A piece of a file rendered with `oxy` conflict markers (spec §9.5). |
|
| 22 | 29 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 23 | 30 | pub enum Piece { |
|
| … | 200 unchanged lines |
||
| 224 | 231 | path: String, |
|
| 225 | 232 | state: String, |
|
| 226 | 233 | candidates: Vec<Candidate>, |
|
| 234 | + | vscode: String, |
|
| 227 | 235 | delegation: Option<String>, |
|
| 228 | 236 | error: Option<String>, |
|
| 229 | 237 | message: String, |
|
| … | 8 unchanged lines |
||
| 238 | 246 | path: String, |
|
| 239 | 247 | state: String, |
|
| 240 | 248 | pieces: Vec<Piece>, |
|
| 249 | + | /// The link that hands this conflict to oxygen-vscode. |
|
| 250 | + | vscode: String, |
|
| 241 | 251 | delegation: Option<String>, |
|
| 242 | 252 | error: Option<String>, |
|
| 243 | 253 | message: String, |
|
| … | 68 unchanged lines |
||
| 312 | 322 | path: path.to_string(), |
|
| 313 | 323 | state, |
|
| 314 | 324 | pieces, |
|
| 325 | + | vscode: vscode_link(app, owner, name, bookmark, path), |
|
| 315 | 326 | delegation, |
|
| 316 | 327 | error, |
|
| 317 | 328 | message: message.unwrap_or_else(|| format!("Resolve the conflicts in {path}")), |
|
| … | 41 unchanged lines |
||
| 359 | 370 | path: path.to_string(), |
|
| 360 | 371 | state, |
|
| 361 | 372 | candidates, |
|
| 373 | + | vscode: vscode_link(app, owner, name, bookmark, path), |
|
| 362 | 374 | delegation, |
|
| 363 | 375 | error, |
|
| 364 | 376 | message: message.unwrap_or_else(|| format!("Resolve the name conflict at {path}")), |
|
| … | 133 unchanged lines |
modified crates/hub/tests/edits.rs +7 −1
| … | 264 unchanged lines |
||
| 265 | 265 | "{}", |
|
| 266 | 266 | page.body |
|
| 267 | 267 | ); |
|
| 268 | + | assert!( |
|
| 269 | + | page.body.contains("vscode://oxygen.oxygen-vscode/resolve?url=") && page.body.contains("&bookmark=main&path=a.txt"), |
|
| 270 | + | "{}", |
|
| 271 | + | page.body |
|
| 272 | + | ); |
|
| 268 | 273 | let state = { |
|
| 269 | 274 | let i = page.body.find("name=\"state\" value=\"").unwrap() + "name=\"state\" value=\"".len(); |
|
| 270 | 275 | page.body[i..i + 64].to_string() |
|
| … | 53 unchanged lines |
||
| 324 | 329 | page.body[i..i + 64].to_string() |
|
| 325 | 330 | }; |
|
| 326 | 331 | // Which suffix holds "second" depends on node order; keep that one. |
|
| 327 | - | let second = if page.body.find("second").unwrap() < page.body.find("first").unwrap() { 0 } else { 1 }; |
|
| 332 | + | let preview = |text: &str| page.body.find(&format!("<pre class=\"marked\">{text}")).expect("a candidate's preview"); |
|
| 333 | + | let second = if preview("second") < preview("first") { 0 } else { 1 }; |
|
| 328 | 334 | let r = send( |
|
| 329 | 335 | app, |
|
| 330 | 336 | "POST", |
|
| … | 33 unchanged lines |
modified docs/spec.md +1 −1
| … | 292 unchanged lines |
||
| 293 | 293 | ||
| 294 | 294 | ### 6.10 Conflicts page |
|
| 295 | 295 | ||
| 296 | - | For each bookmark whose state has conflicts: the file (or tree entry), the conflict kind, and each side with the change that produced it and its content, rendered with the same marker structure `oxy checkout` writes. For a text file, **resolve region by region** (`/{owner}/{name}/resolve/{bookmark}/{path}`, writers) shows the lines no one disagrees on and each conflict region with its sides and the change that wrote each; per region the user keeps one side, every side in order, none, or their own text (a side that records a deletion keeps the deletion). Regions are linkable (`#region-N`). The file is put back together and committed through the delegate as a web edit (§6.9), which makes it a `kind = resolution` change depending on every contributor; the page carries the bookmark's state and refuses to commit if the bookmark moved meanwhile. Malformed markers fall back to the editor, which also stays available: "edit the marked file" opens the markers in it. A name conflict (several files want one name, shown as `NAME.oxy-conflict-N`) resolves on the same URL by keeping one candidate: it takes the name as a move, keeping its identity, and the other candidates are removed. Conflicts between directories or links, and replace conflicts of binary files, are resolved from a client. The change page tags resolution changes. Otherwise the page gives the two-line client recipe. |
|
| 296 | + | For each bookmark whose state has conflicts: the file (or tree entry), the conflict kind, and each side with the change that produced it and its content, rendered with the same marker structure `oxy checkout` writes. For a text file, **resolve region by region** (`/{owner}/{name}/resolve/{bookmark}/{path}`, writers) shows the lines no one disagrees on and each conflict region with its sides and the change that wrote each; per region the user keeps one side, every side in order, none, or their own text (a side that records a deletion keeps the deletion). Regions are linkable (`#region-N`). The file is put back together and committed through the delegate as a web edit (§6.9), which makes it a `kind = resolution` change depending on every contributor; the page carries the bookmark's state and refuses to commit if the bookmark moved meanwhile. Malformed markers fall back to the editor, which also stays available: "edit the marked file" opens the markers in it. A name conflict (several files want one name, shown as `NAME.oxy-conflict-N`) resolves on the same URL by keeping one candidate: it takes the name as a move, keeping its identity, and the other candidates are removed. Conflicts between directories or links, and replace conflicts of binary files, are resolved from a client. Both resolver pages offer **Resolve in VS Code**, a `vscode://oxygen.oxygen-vscode/resolve?url=…&bookmark=…&path=…` link that oxygen-vscode handles: it finds the open checkout of that repository (or offers to clone it), switches to the bookmark after asking, pulls it, and opens the file. The change page tags resolution changes. Otherwise the page gives the two-line client recipe. |
|
| 297 | 297 | ||
| 298 | 298 | ### 6.11 Trust page |
|
| 299 | 299 | ||
| … | 255 unchanged lines |
modified templates/resolve.html +1 −0
| … | 6 unchanged lines |
||
| 7 | 7 | {% block content %} |
|
| 8 | 8 | <div class="head"><h1>Resolve <code>{{ path }}</code> <small>on <span class="lbl lbl-bm">{{ bookmark }}</span></small></h1> |
|
| 9 | 9 | <p class="small mute">Choose what each conflict keeps. The file is put back together around the lines no one disagrees on and committed to {{ bookmark }} as a resolution: it depends on every change that wrote a side, so no side is lost from history. Link to a conflict with its <code>#region-N</code>.</p></div> |
|
| 10 | + | <p class="small"><a href="{{ vscode }}">Resolve in VS Code</a> <span class="mute">(with the Oxygen extension: it opens your checkout on {{ bookmark }}, or offers to clone it)</span></p> |
|
| 10 | 11 | {% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %} |
|
| 11 | 12 | {% if let Some(d) = delegation %}<p class="error">{{ d }}</p>{% endif %} |
|
| 12 | 13 | <form method="post" action="{{ repo.url }}/resolve/{{ bookmark }}/{{ path }}"> |
|
| … | 29 unchanged lines |
modified templates/resolve_name.html +1 −0
| … | 6 unchanged lines |
||
| 7 | 7 | {% block content %} |
|
| 8 | 8 | <div class="head"><h1>Resolve <code>{{ path }}</code> <small>on <span class="lbl lbl-bm">{{ bookmark }}</span></small></h1> |
|
| 9 | 9 | <p class="small mute">{{ candidates.len() }} files want this name, so the tree shows them as <code>{{ path }}.oxy-conflict-N</code>. Keep one: it takes the name, keeping its history, and the others are removed. The commit is a resolution change on {{ bookmark }}.</p></div> |
|
| 10 | + | <p class="small"><a href="{{ vscode }}">Resolve in VS Code</a> <span class="mute">(with the Oxygen extension: it opens your checkout on {{ bookmark }}, or offers to clone it)</span></p> |
|
| 10 | 11 | {% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %} |
|
| 11 | 12 | {% if let Some(d) = delegation %}<p class="error">{{ d }}</p>{% endif %} |
|
| 12 | 13 | <form method="post" action="{{ repo.url }}/resolve/{{ bookmark }}/{{ path }}"> |
|
| … | 12 unchanged lines |