OXYGENHUB

Resolutions depend only on contributors; candidate-order-independent name conflict test b288cd254343

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-26 18:18 UTC
Signature
✔ signed by Andy McCall
Hash
b288cd254343a8c981e7c891d9cbd4472d208a1cb643e9be69c71c479b4c3fa8
Depends on
20247a03a596 ff0af1ee299d
Depended on by
nothing yet
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
7 atoms, 0 tree operations, 1 file

Browse the tree at this change

modified crates/oxy-core/src/repo.rs +16 −18
…
167 unchanged lines
168 168
    pub changed: bool,
169 169
    /// Whether the working copy resolves conflicts of its base (spec §9.6).
170 170
    pub resolves: bool,
171 +
    /// Changes that contributed to the conflicts being resolved.
172 +
    pub contributors: Vec<ChangeHash>,
171 173
}
172 174
173 175
pub struct LogEntry {
…
421 unchanged lines
595 597
        wc.deps = deps.into_iter().collect();
596 598
        wc.normalize();
597 599
        let resolves = b.resolves;
600 +
        let contributors: Vec<ChangeHash> = b.contributors.iter().copied().collect();
598 601
599 602
        let mut statuses = b.statuses;
600 603
        statuses.sort_by(|a, b| a.path.cmp(&b.path));
…
6 unchanged lines
607 610
            view.workspaces.insert(self.workspace, new_hash);
608 611
            self.record_op(&[op_ref], &format!("{command}{SNAPSHOT_SUFFIX}"), view)?;
609 612
        }
610 -
        Ok(SnapshotResult { wc_hash: new_hash, wc, base, base_snapshot, statuses, changed, resolves })
613 +
        Ok(SnapshotResult { wc_hash: new_hash, wc, base, base_snapshot, statuses, changed, resolves, contributors })
611 614
    }
612 615
613 616
    /// Turn the working-copy change into a normal change on the bookmark (spec §12.3).
…
10 unchanged lines
624 627
        }
625 628
        let (op_ref, op) = self.head_op()?;
626 629
        let mut deps: BTreeSet<ChangeHash> = referenced_changes(&snap.wc, &snap.base);
627 -
        if snap.resolves {
628 -
            // A resolution depends on every change that contributed to the
629 -
            // conflict (spec §8.1); those are already among the working copy's
630 -
            // dependencies, so keep every one that is not merely a base head.
631 -
            let heads: BTreeSet<ChangeHash> = snap.base.maximal(&snap.wc.deps).into_iter().collect();
632 -
            deps.extend(snap.wc.deps.iter().filter(|d| !heads.contains(d) || touches_conflict(&snap)).copied());
633 -
        }
630 +
        // A resolution depends on every change that contributed to the
631 +
        // conflict (spec §8.1). Tree and opaque conflicts are resolved through
632 +
        // ordinary operations whose references already cover the contributors.
633 +
        deps.extend(snap.contributors.iter().copied());
634 634
        for d in extra_deps {
635 635
            if !snap.base.contains(d) {
636 636
                return fail(format!("--depends-on {d}: not in the working copy's base state"));
…
489 unchanged lines
1126 1126
        }
1127 1127
        Ok(())
1128 1128
    }
1129 -
}
1130 -
1131 -
/// Whether a working copy touches a node that is conflicted in its base.
1132 -
fn touches_conflict(snap: &SnapshotResult) -> bool {
1133 -
    let conflicted: BTreeSet<NodeId> = snap.base_snapshot.files.values().filter(|f| f.is_conflicted()).map(|f| f.node).collect();
1134 -
    snap.wc.tree_ops.iter().any(|op| conflicted.contains(&op.node())) || snap.wc.edits.iter().any(|e| conflicted.contains(&e.node()))
1135 1129
}
1136 1130
1137 1131
fn repo_live_keys(base: &State, node: &NodeId) -> Vec<crate::graph::AtomKey> {
…
340 unchanged lines
1478 1472
        let h4 = repo.commit("resolve", &[]).unwrap();
1479 1473
        let c4 = repo.store().get_change(&h4).unwrap();
1480 1474
        assert_eq!(c4.kind, ChangeKind::Resolution);
1481 -
        assert!(c4.deps.contains(&h2) && c4.deps.contains(&h3) && c4.deps.contains(&h1), "{:?}", c4.deps);
1475 +
        let mut want = vec![h1, h2, h3];
1476 +
        want.sort();
1477 +
        assert_eq!(c4.deps, want, "a resolution depends on exactly the contributors and what it references");
1482 1478
        assert_eq!(repo.bookmark_heads().unwrap(), vec![h4]);
1483 1479
        let snap = repo.load_state(&[h4]).unwrap().materialize();
1484 1480
        assert!(!snap.is_conflicted());
…
45 unchanged lines
1530 1526
        assert!(r.conflicted);
1531 1527
        assert!(root.join("new.txt.oxy-conflict-1").exists() && root.join("new.txt.oxy-conflict-2").exists());
1532 1528
        assert!(!root.join("new.txt").exists());
1533 -
        // Keep candidate 2 under the real name.
1534 -
        fs::remove_file(root.join("new.txt.oxy-conflict-1")).unwrap();
1535 -
        fs::rename(root.join("new.txt.oxy-conflict-2"), root.join("new.txt")).unwrap();
1529 +
        // Candidate numbering follows change-hash order, so pick by content:
1530 +
        // keep the "two" candidate under the real name and drop the other.
1531 +
        let (keep, drop) = if fs::read(root.join("new.txt.oxy-conflict-1")).unwrap() == b"two\n" { (1, 2) } else { (2, 1) };
1532 +
        fs::remove_file(root.join(format!("new.txt.oxy-conflict-{drop}"))).unwrap();
1533 +
        fs::rename(root.join(format!("new.txt.oxy-conflict-{keep}")), root.join("new.txt")).unwrap();
1536 1534
        let h = repo.commit("pick two", &[]).unwrap();
1537 1535
        assert_eq!(repo.store().get_change(&h).unwrap().kind, ChangeKind::Resolution);
1538 1536
        // The resolution does not touch keep.txt, so it does not depend on h1;
…
203 unchanged lines