OXYGENHUB

Marker-like text no longer blocks a conflict resolution 3652104ae7d3

Resolving proposal #125's conflict in docs/spec.md was refused ('markers
remain') because §9.5 holds a fenced example of oxy's own conflict
markers: any line starting '<<<<<<< oxy conflict' counted. A file now
still has markers only when it holds one of the marker lines oxy wrote
for its conflicts: the marker-like lines of the materialized file that
are not among its unconflicted lines. Documentation or fixtures that
show markers no longer stop a resolution; a real marker left behind is
still caught.

Closes #126
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 433af17e3790
When
· 2026-10-18 20:19 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
3652104ae7d3a03e79396c304e66cec0b979896bb6be16e45c7f74d327b52d5e
Depends on
75167672ebcd 982864068a59 cdb5beba7f05
Depended on by
9bd98cfb4f7e
In bookmarks
doctor-trust identical-sides keyless-tags main push-notes subrepo-pin-state
In tags
v0.10.0 v0.11.0 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
5 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/snapshot.rs +31 −2
…
25 unchanged lines
26 26
    })
27 27
}
28 28
29 +
/// Whether `content` still holds a conflict marker oxy wrote for this file
30 +
/// (oxygen#126): a marker-like line of the materialized conflicted file
31 +
/// that is not one of its unconflicted lines (`plain`). Text that merely
32 +
/// looks like a marker, as oxy's own documentation or a test fixture has,
33 +
/// is in the unconflicted lines, so it is not taken for one.
34 +
pub fn has_own_conflict_markers(content: &[u8], materialized: &[u8], plain: &[&[u8]]) -> bool {
35 +
    let line = |l: &[u8]| -> Vec<u8> {
36 +
        let l = l.strip_suffix(b"\n").unwrap_or(l);
37 +
        l.strip_suffix(b"\r").unwrap_or(l).to_vec()
38 +
    };
39 +
    let marker = |l: &[u8]| {
40 +
        l.starts_with(b"<<<<<<< oxy conflict")
41 +
            || l.starts_with(b"======= oxy conflict")
42 +
            || l.starts_with(b">>>>>>> oxy conflict")
43 +
    };
44 +
    let unconflicted: HashSet<Vec<u8>> = plain.iter().map(|l| line(l)).collect();
45 +
    let own: HashSet<Vec<u8>> = split_lines(materialized)
46 +
        .into_iter()
47 +
        .map(line)
48 +
        .filter(|l| marker(l) && !unconflicted.contains(l))
49 +
        .collect();
50 +
    !own.is_empty()
51 +
        && split_lines(content)
52 +
            .into_iter()
53 +
            .any(|l| own.contains(&line(l)))
54 +
}
55 +
29 56
#[derive(Clone, Debug, PartialEq, Eq)]
30 57
pub struct PathStatus {
31 58
    pub path: String,
…
1467 unchanged lines
1499 1526
                    if bytes == &disk_text {
1500 1527
                        return Ok(());
1501 1528
                    }
1502 -
                    if has_conflict_markers(&e.content) {
1529 +
                    let regions = self.base.conflict_regions(&node);
1530 +
                    let plain: Vec<&[u8]> =
1531 +
                        regions.plain.iter().map(|(_, l)| l.as_slice()).collect();
1532 +
                    if has_own_conflict_markers(&e.content, bytes, &plain) {
1503 1533
                        self.statuses.push(PathStatus {
1504 1534
                            path: path.to_string(),
1505 1535
                            status: ChangeStatus::ConflictEdited,
…
4 unchanged lines
1510 1540
                    // with each region as a gap, then retire every region atom.
1511 1541
                    // Whatever the author kept from a region is re-inserted as
1512 1542
                    // new atoms, which leaves the graph totally ordered.
1513 -
                    let regions = self.base.conflict_regions(&node);
1514 1543
                    let mut edits = text_edits(node, &regions.plain, &disk_text);
1515 1544
                    if !regions.region_atoms.is_empty() {
1516 1545
                        edits.insert(
…
474 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +59 −0
…
170 unchanged lines
171 171
    (dir, repo, h1, h2, h3)
172 172
}
173 173
174 +
/// oxygen#126: a file whose text includes an example of oxy's markers (as
175 +
/// the spec's §9.5 does) can still have its conflict resolved; a real
176 +
/// marker left behind is still caught.
174 177
#[test]
178 +
fn marker_like_text_does_not_block_a_resolution() {
179 +
    hermetic();
180 +
    let dir = tempfile::tempdir().unwrap();
181 +
    let root = dir.path();
182 +
    let repo = Repo::init(root).unwrap();
183 +
    let example = "```\n<<<<<<< oxy conflict 1 of 2, side 1: change k7m2p9 \"Add retry loop\"\n... lines ...\n======= oxy conflict 1 of 2, side 2: change q3x8w1 \"Refactor client\"\n>>>>>>> oxy conflict 1 of 2 end\n```\n";
184 +
    write(
185 +
        root,
186 +
        "doc.md",
187 +
        format!("intro\n{example}b\nend\n").as_bytes(),
188 +
    );
189 +
    let h1 = repo.commit("base", &[]).unwrap();
190 +
    write(
191 +
        root,
192 +
        "doc.md",
193 +
        format!("intro\n{example}B\nend\n").as_bytes(),
194 +
    );
195 +
    repo.commit("upper", &[]).unwrap();
196 +
    repo.checkout(&[h1], false, "checkout").unwrap();
197 +
    write(
198 +
        root,
199 +
        "doc.md",
200 +
        format!("intro\n{example}X\nend\n").as_bytes(),
201 +
    );
202 +
    repo.commit("ex", &[]).unwrap();
203 +
    repo.checkout(&repo.bookmark_heads().unwrap(), false, "checkout main")
204 +
        .unwrap();
205 +
    let marked = fs::read_to_string(root.join("doc.md")).unwrap();
206 +
    assert!(marked.contains("oxy conflict 1 of 1"), "{marked}");
207 +
    // A real marker left: still caught.
208 +
    write(root, "doc.md", marked.replace("B\n", "BB\n").as_bytes());
209 +
    let s = repo.snapshot("status").unwrap();
210 +
    assert_eq!(
211 +
        statuses(&s),
212 +
        vec![("doc.md".into(), ChangeStatus::ConflictEdited)]
213 +
    );
214 +
    // Resolved, the example kept: a resolution.
215 +
    write(
216 +
        root,
217 +
        "doc.md",
218 +
        format!("intro\n{example}X\nend\n").as_bytes(),
219 +
    );
220 +
    let s = repo.snapshot("status").unwrap();
221 +
    assert_eq!(
222 +
        statuses(&s),
223 +
        vec![("doc.md".into(), ChangeStatus::Resolved)]
224 +
    );
225 +
    let h = repo.commit("resolve", &[]).unwrap();
226 +
    assert_eq!(
227 +
        repo.store().get_change(&h).unwrap().kind,
228 +
        ChangeKind::Resolution
229 +
    );
230 +
    assert!(!repo.load_state(&[h]).unwrap().materialize().is_conflicted());
231 +
}
232 +
233 +
#[test]
175 234
fn concurrent_edits_conflict_and_can_be_resolved() {
176 235
    hermetic();
177 236
    let (dir, repo, h1, h2, h3) = conflicted_repo();
…
3693 unchanged lines