OXYGENHUB

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("&#38;bookmark=main&#38;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