OXYGENHUB

Revsets: independent() and conflicts() 543bbf64d127

independent(X, IN) is what could ship without X: the members of IN
that neither depend on X nor are depended on by it. conflicts(S) is
the changes that contributed atoms to a conflict in the state S names
(the working copy's base when S is left out): text regions through
conflict_regions, opaque replace conflicts through blob_contributors.
Tests: c and d are independent of b; two edits of one line make b
and e the contributors of the conflict.

refs #65
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-04 03:21 UTC
Signature
✔ signed by Andy McCall
Hash
543bbf64d127d15e7be2d6692d8f5c8e155f48cc555c4f53dbaf89f07957e749
Depends on
2c9ddac3c387 47ce8b8e2d4b 8ad9dfe16303 982864068a59
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.10.0 v0.11.0 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, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/revset.rs +61 −0
…
367 unchanged lines
368 368
                let x = self.eval(&args[0], None)?;
369 369
                self.roots(&x)
370 370
            }
371 +
            "independent" => {
372 +
                self.arity(name, args, 2)?;
373 +
                let x = self.eval(&args[0], None)?;
374 +
                let inn = self.eval(&args[1], None)?;
375 +
                let below = self.ancestors(&x)?;
376 +
                let above = self.descendants(&x)?;
377 +
                Ok(inn
378 +
                    .into_iter()
379 +
                    .filter(|h| !below.contains(h) && !above.contains(h))
380 +
                    .collect())
381 +
            }
382 +
            "conflicts" => {
383 +
                if args.len() > 1 {
384 +
                    return Err(err(
385 +
                        "conflicts() takes a state, or nothing for the working copy's",
386 +
                    ));
387 +
                }
388 +
                let heads: Vec<ChangeHash> = match args.first() {
389 +
                    Some(e) => self.eval(e, None)?.into_iter().collect(),
390 +
                    None => match self.working_copy() {
391 +
                        Some(wc) => {
392 +
                            self.ensure(&[wc])?;
393 +
                            self.graph
394 +
                                .get(&wc)
395 +
                                .map(|m| m.deps.clone())
396 +
                                .unwrap_or_default()
397 +
                        }
398 +
                        None => self.repo.bookmark_heads()?,
399 +
                    },
400 +
                };
401 +
                self.conflicts(within, &heads)
402 +
            }
371 403
            "latest" => {
372 404
                self.arity(name, args, 2)?;
373 405
                let x = self.eval(&args[0], None)?;
…
202 unchanged lines
576 608
            self.tick()?;
577 609
        }
578 610
        self.closure_of(heads, within)
611 +
    }
612 +
613 +
    /// `conflicts(S)`: the changes that contributed atoms to a conflict in
614 +
    /// the state `heads` name (conflicts belong to states, not changes).
615 +
    fn conflicts(&mut self, within: Option<&Set>, heads: &[ChangeHash]) -> Result<Set> {
616 +
        let state = self.repo.load_state(heads)?;
617 +
        let snapshot = state.materialize();
618 +
        let mut out = Set::new();
619 +
        for f in snapshot.files.values() {
620 +
            if !f.is_conflicted() {
621 +
                continue;
622 +
            }
623 +
            match &f.content {
624 +
                crate::state::FileContent::Text { .. } => {
625 +
                    out.extend(state.conflict_regions(&f.node).contributors);
626 +
                }
627 +
                crate::state::FileContent::Opaque { blobs } => {
628 +
                    out.extend(state.blob_contributors(&f.node, blobs));
629 +
                }
630 +
                crate::state::FileContent::Dir => {}
631 +
            }
632 +
            self.tick()?;
633 +
        }
634 +
        self.ensure(heads)?;
635 +
        Ok(match within {
636 +
            Some(w) => out.intersection(w).copied().collect(),
637 +
            None => out,
638 +
        })
579 639
    }
580 640
581 641
    /// `filter_metas` for a test that needs the evaluator (identity lookups).
…
35 unchanged lines
617 677
                    | "revoked"
618 678
                    | "attested"
619 679
                    | "built"
680 +
                    | "conflicts"
620 681
            ),
621 682
            Expr::Intersection(a, b) => Self::is_predicate(a) || Self::is_predicate(b),
622 683
            Expr::Union(a, b) => Self::is_predicate(a) && Self::is_predicate(b),
…
123 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +30 −0
…
7146 unchanged lines
7147 7147
    repo.revoke_identity(&key, "test").unwrap();
7148 7148
    assert_eq!(set("revoked()"), vec![d]);
7149 7149
    assert_eq!(set("signed_by(me) ~ revoked()"), vec![]);
7150 +
    // independent(X, IN): what could ship without X. c and d neither
7151 +
    // depend on b nor are depended on by it; a is below, the working copy
7152 +
    // above.
7153 +
    assert_eq!(
7154 +
        set(&format!("independent(#{}, ::@)", b.short(12))),
7155 +
        sorted(&[c, d])
7156 +
    );
7157 +
    assert_eq!(
7158 +
        set(&format!("independent(#{}, ::main)", a.short(12))),
7159 +
        vec![c]
7160 +
    );
7161 +
    // conflicts(S): nothing conflicts yet; then two edits of one line.
7162 +
    assert_eq!(set("conflicts()"), vec![]);
7163 +
    assert_eq!(set("conflicts(main)"), vec![]);
7164 +
    // (The saved revset above edited the user config under the root:
7165 +
    // discard it.)
7166 +
    repo.checkout(&[a], true, "checkout a").unwrap();
7167 +
    write(root, "src/a.rs", b"one\nTWO\n");
7168 +
    let e = repo
7169 +
        .commit("fifth: the other edit of line two", &[])
7170 +
        .unwrap();
7171 +
    repo.checkout(&repo.bookmark_heads().unwrap(), true, "checkout main")
7172 +
        .unwrap();
7173 +
    let conflicted = repo.revset("conflicts()").unwrap().hashes;
7174 +
    assert!(
7175 +
        conflicted.contains(&e) && conflicted.contains(&b),
7176 +
        "{conflicted:?} should hold b and e"
7177 +
    );
7178 +
    assert!(!conflicted.contains(&c));
7179 +
    assert_eq!(set("conflicts() & author(nobody)"), vec![]);
7150 7180
}
modified docs/revset_query_language.md +3 −3
…
26 unchanged lines
27 27
**Implemented (2026-10-04, oxygen#65):** `oxy log -r`, `oxy show -r`,
28 28
`oxy verify -r` and `oxy abandon -r` with the symbols, relations,
29 29
operators, `author`, `description` (substring), `date`, `kind`,
30 -
`touches`, `latest` and the trust predicates of §2.4 below, saved
31 -
revsets, and a time budget. Not yet: `independent()`, `conflicts()`,
30 +
`touches`, `latest`, `independent`, `conflicts` and the trust predicates
31 +
of §2.4 below, saved revsets, and a time budget. Not yet:
32 32
`description(re:…)`, `diff -r`, `push -r`, `fold`, and the hub's `?r=`.
33 33
Other commands take a single change (a ChangeId prefix, a `#hash`
34 34
prefix or `@`) or a bookmark or tag name; symbols in a revset resolve
…
45 unchanged lines
80 80
| `date(RANGE)` | changes by HLC wall time: `2026-10-01..`, `-7d..`, `..2026-09-30` |
81 81
| `kind(K)` | `normal`, `resolution` or `working-copy` |
82 82
| `touches(GLOB)` | changes that edit any node whose path matches GLOB in the evaluation state. Nodes keep their identity through renames, so this follows a file's whole history under any name. |
83 -
| `conflicts(S)` | changes that contributed atoms to a conflict in state S (default `@`). Conflicts belong to states, not changes. |
83 +
| `conflicts(S)` | changes that contributed atoms to a conflict in state S (default: the working copy's base). Conflicts belong to states, not changes. |
84 84
85 85
### 2.4 Trust: unique to Oxygen
86 86
…
76 unchanged lines