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 |