OXYGENHUB

Repo::union_resolution: a host settles a state whose conflicts are all union (refs #145) ac4ec124b9f2

For OxygenHub's land: the kept-both text of every conflicted file when
each is a merge=union text file and each conflict an adjacent insertion;
None when any conflict needs a person.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key b62ca20efc42
When
· 2026-10-26 10:15 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
ac4ec124b9f2fa52ea27d682b54225cf95a771daf8fb4ab7f5ab53410ef19286
Depends on
157d02e95586 4afe288b7946 982864068a59 9bd98cfb4f7e
Depended on by
c0d4075cadc0
In bookmarks
identical-sides main push-notes subrepo-pin-state
In tags
v0.10.0 v0.11.0 v0.8.0 v0.9.0
Edits
2 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/snapshot.rs +33 −0
…
639 unchanged lines
640 640
        Ok(kept.into_iter().filter(|k| !k.settled.is_empty()).collect())
641 641
    }
642 642
643 +
    /// Every conflict of `snap`, `state`'s materialization, settled by
644 +
    /// keeping both sides (oxygen#145): the new text of each conflicted
645 +
    /// file, when each is a text file its `.oxyattributes` mark
646 +
    /// `merge=union` and each of its conflicts is an adjacent insertion;
647 +
    /// `None` when any conflict needs a person, or there is none. A host
648 +
    /// lands a proposal with these as a resolution, as a checkout settles
649 +
    /// them in a working copy.
650 +
    pub fn union_resolution(state: &State, snap: &Snapshot) -> Option<Vec<(String, Vec<u8>)>> {
651 +
        if snap.tree.is_conflicted() {
652 +
            return None;
653 +
        }
654 +
        let attrs = Repo::state_attributes(snap);
655 +
        let mut out = Vec::new();
656 +
        for (p, f) in &snap.files {
657 +
            if !f.is_conflicted() {
658 +
                continue;
659 +
            }
660 +
            let FileContent::Text { bytes, .. } = &f.content else {
661 +
                return None;
662 +
            };
663 +
            if !attrs.lookup(p).union {
664 +
                return None;
665 +
            }
666 +
            let sides = state.both_sides(&f.node);
667 +
            let (text, _, left) = keep_both_text(bytes, &sides, false);
668 +
            if !left.is_empty() || sides.iter().any(Option::is_none) {
669 +
                return None;
670 +
            }
671 +
            out.push((p.clone(), text));
672 +
        }
673 +
        (!out.is_empty()).then_some(out)
674 +
    }
675 +
643 676
    /// The chosen resolutions `base` still needs; the rest are forgotten.
644 677
    fn resolutions_for(&self, base: &Snapshot) -> Result<HashSet<String>> {
645 678
        let all = self.resolutions()?;
…
2768 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +33 −0
…
334 unchanged lines
335 335
    );
336 336
}
337 337
338 +
/// oxygen#145: a host (OxygenHub landing a proposal) settles a state whose
339 +
/// only conflicts are adjacent insertions in union files; any other
340 +
/// conflict needs a person.
341 +
#[test]
342 +
fn a_host_settles_a_state_whose_conflicts_are_all_union() {
343 +
    hermetic();
344 +
    let dir = tempfile::tempdir().unwrap();
345 +
    let root = dir.path();
346 +
    let repo = Repo::init(root).unwrap();
347 +
    write(root, ".oxyattributes", b"*.md merge=union\n");
348 +
    write(root, "A.md", b"a\nb\n");
349 +
    write(root, "B.md", b"x\ny\n");
350 +
    let h1 = repo.commit("base", &[]).unwrap();
351 +
    write(root, "A.md", b"a\none\nb\n");
352 +
    write(root, "B.md", b"x\nY1\n");
353 +
    let h2 = repo.commit("one", &[]).unwrap();
354 +
    repo.checkout(&[h1], false, "checkout").unwrap();
355 +
    write(root, "A.md", b"a\ntwo\nb\n");
356 +
    let h3 = repo.commit("two", &[]).unwrap();
357 +
    let st = repo.load_state(&[h2, h3]).unwrap();
358 +
    assert_eq!(
359 +
        Repo::union_resolution(&st, &st.materialize()),
360 +
        Some(vec![("A.md".into(), b"a\none\ntwo\nb\n".to_vec())])
361 +
    );
362 +
    // B.md's line replaced two ways: a person decides.
363 +
    repo.checkout(&[h3], false, "checkout").unwrap();
364 +
    write(root, "B.md", b"x\nY2\n");
365 +
    let h4 = repo.commit("Y2", &[]).unwrap();
366 +
    let st = repo.load_state(&[h2, h4]).unwrap();
367 +
    assert!(st.materialize().is_conflicted());
368 +
    assert_eq!(Repo::union_resolution(&st, &st.materialize()), None);
369 +
}
370 +
338 371
/// oxygen#121: two changes that each replaced the same line are not an
339 372
/// adjacent insertion: keeping both would keep two versions of the line,
340 373
/// so `--both` leaves the markers, as it does for markers already edited.
…
10211 unchanged lines