OXYGENHUB

Rebase derives the change from the target state with and without it, so the target's own lines survive; spec wording to match; the previous commit's test was failing 8b678b298fee

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-27 14:33 UTC
Signature
✔ signed by Andy McCall
Hash
8b678b298fee1e3e20e8f84de25768af4ad6f44246968c076c362fe174def8f0
Depends on
614541c3cebd 728da0f92161 86fe29fd62ea 982864068a59 ff0af1ee299d
Depended on by
e266b7d4065c 06d16dc2bc13 75167672ebcd
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
8 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/history.rs +13 −7
…
760 unchanged lines
761 761
        }
762 762
        let onto = target.maximal(onto);
763 763
        let target_snapshot = target.materialize();
764 -
        let before = self.load_state(&old.deps)?.materialize();
765 -
        let after = self.load_state(&[*c])?.materialize();
766 -
        // The paths whose file differs between the state with and without c.
764 +
        // c applied to the target: the model does this by union, since c
765 +
        // commutes with everything it does not depend on. What the union
766 +
        // shows is what the re-derived change must produce.
767 +
        let mut with_c = target.clone();
768 +
        if with_c.add(old.clone()).is_err() {
769 +
            return fail(format!("the target state lacks dependencies of #{}; rebase onto a state that has them", c.short(12)));
770 +
        }
771 +
        let after = with_c.materialize();
772 +
        // The paths whose file differs between the target and the target with c.
767 773
        let mut touched: BTreeSet<String> = BTreeSet::new();
768 -
        for p in before.files.keys().chain(after.files.keys()) {
769 -
            match (before.files.get(p), after.files.get(p)) {
774 +
        for p in target_snapshot.files.keys().chain(after.files.keys()) {
775 +
            match (target_snapshot.files.get(p), after.files.get(p)) {
770 776
                (Some(a), Some(b)) if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {}
771 777
                _ => {
772 778
                    touched.insert(p.clone());
…
1 unchanged lines
774 780
            }
775 781
        }
776 782
        for p in &touched {
777 -
            if target_snapshot.files.get(p).is_some_and(|f| f.is_conflicted()) {
778 -
                return fail(format!("{p} is conflicted in the target state; resolve it first"));
783 +
            if target_snapshot.files.get(p).is_some_and(|f| f.is_conflicted()) || after.files.get(p).is_some_and(|f| f.is_conflicted()) {
784 +
                return fail(format!("{p} would be conflicted; resolve the target state or the conflict with #{} first", c.short(12)));
779 785
            }
780 786
        }
781 787
        // Entries: the target's files, with touched paths as c leaves them.
…
55 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +1 −1
…
2529 unchanged lines
2530 2530
    // Rebasing onto a state that already contains the change is refused.
2531 2531
    assert!(repo.rebase(&c2, &[c2]).is_err());
2532 2532
    let err = repo.rebase(&e2, &[d]).unwrap_err().to_string();
2533 -
    assert!(err.contains("empty") || err.contains("already"), "{err}");
2533 +
    assert!(err.contains("lacks dependencies"), "E needs C, which D's state does not have: {err}");
2534 2534
}
2535 2535
modified docs/spec.md +1 −1
…
337 unchanged lines
338 338
339 339
**Squash.** `oxy squash C --into D` folds change `C` into `D`, where `D` is a dependency of `C` and every atom, placement, and mode that `C` refers to in `D` is one `D` introduces or one both already know. The result `D'` carries `D`'s operations followed by `C`'s, with `C`'s references to `D` turned into self-references, keeps `D`'s ChangeId, and is signed afresh by its author; `C` is removed from the state and every change that depended on `C` or `D` is rewritten with the correspondence of §8.4. A `C` that depends on changes `D` does not depend on cannot be squashed into `D`; the command names them. `oxy amend --into` is the special case where `C` is the working copy.
340 340
341 -
**Rebase.** `oxy rebase C --onto HEADS` re-expresses `C` against a different base: the files `C` touches are materialized twice, from `closure(deps(C))` and from `closure(deps(C)) ∪ {C}`, the difference is re-derived as edits against the state `HEADS` names, and the new change keeps `C`'s ChangeId with dependencies computed from what it now references (plus `HEADS` as explicit dependencies). This is the move a Git user makes with `rebase --onto`: it changes what a change is said to build on. It is refused when `HEADS` conflicts in any file `C` touches, since the re-derivation would have no single text to diff against, and when the result would be empty. Dependents of `C` are rewritten per §8.4. Because commuting changes already coexist, the common reason to rebase is to drop an accidental dependency, for instance on a change that is to be abandoned.
341 +
**Rebase.** `oxy rebase C --onto HEADS` re-expresses `C` against a different base: the state `HEADS` names is materialized with and without `C` added to it (commutation makes the union meaningful, and `HEADS` must contain `C`'s dependencies), the difference is re-derived as edits against the `HEADS` state, and the new change keeps `C`'s ChangeId with dependencies computed from what it now references (plus `HEADS` as explicit dependencies). This is the move a Git user makes with `rebase --onto`: it changes what a change is said to build on. It is refused when `HEADS` conflicts in any file `C` touches, since the re-derivation would have no single text to diff against, and when the result would be empty. Dependents of `C` are rewritten per §8.4. Because commuting changes already coexist, the common reason to rebase is to drop an accidental dependency, for instance on a change that is to be abandoned.
342 342
343 343
## 9. Conflicts
344 344
…
630 unchanged lines