carry: work the target already has is no conflict fe9586d93fb1
checkout --carry and bookmark switch --carry refused when uncommitted files were byte-for-byte what the target state holds (or removed where it has none), as after a proposal of that work landed, leaving only --discard. Such paths are now already applied: no conflict and nothing to carry, and the rest of the work comes along. closes #100
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-11 23:37 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
fe9586d93fb14837bcb44b78acbb490d9dc3ffdde51767e8cab215020bc432a5- Depends on
- 1c2bd9abdd4f 34fad4a92451 61e3bce7ea5b 7b9809e67c3d 8fd78eb52ad2 982864068a59 b689779e9e74
- Depended on by
- 5d79b5c2de88
- In bookmarks
- 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.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
- 5 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified CHANGELOG.md +2 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **`--carry` takes work the target already has (#100).** Switching bookmarks or checking out with `--carry` refused when uncommitted files were byte-for-byte what the target state holds, as after a proposal of that work landed, leaving only `--discard`. Such work is now already applied: no conflict, nothing to carry, and the rest of the work comes along. |
|
| 6 | + | ||
| 5 | 7 | - **Large new files are not snapshotted unseen, and past snapshots expire (#98).** A 1 GB image left untracked in a working tree was taken in by the next `status` and kept by the operation log for good: deleting the file and running `gc --now` freed nothing. Now a new, untracked file over `snapshot.max-new-file-size` (default 100 MiB) is left out and named by `status`, with how to ignore it or raise the limit; tracked files and files already in the working copy are unaffected. Past working-copy snapshots keep their files only for `core.snapshot-retention` (default 7 days), so `gc` prunes what only they held, and `oxy gc --expire-snapshots --now` does it at once. `undo` and `op restore` to an operation whose working copy has expired keep the files as they are. |
|
| 6 | 8 | ||
| 7 | 9 | - **`oxy undo` undoes this workspace's last command (#99).** It undid the repository's newest operation, whichever workspace made it: in a repository with several workspaces an undo could take back another workspace's commit or merge, and it said only which operation it went back to. Operations now record their workspace, and undo takes back this workspace's last command, leaving other workspaces' work as it is. On a bookmark both share, only its own heads go; a change another workspace built on it, or the workspace it would take away, refuses the undo and names it. Undo now says which command it undid and what that changed, passes over pushes (the remote keeps what it was sent), and warns when an undone commit is already on a remote. `oxy amend` warns when it rewrites a change a remote already has, so the next push will need `--force`. |
|
| … | 172 unchanged lines |
modified crates/oxy-core/src/repo/bookmarks.rs +25 −0
| … | 511 unchanged lines |
||
| 512 | 512 | _ => false, |
|
| 513 | 513 | } |
|
| 514 | 514 | }; |
|
| 515 | + | // Work already the target's: the file on disk is exactly the |
|
| 516 | + | // target's version (or both are absent), so nothing needs carrying |
|
| 517 | + | // and it is no conflict (oxygen#100), as when a proposal's work |
|
| 518 | + | // has landed on the bookmark switched to. |
|
| 519 | + | let already = |p: &String| -> bool { |
|
| 520 | + | let full = self.root.join(p); |
|
| 521 | + | match (fs::symlink_metadata(&full), target_state.files.get(p)) { |
|
| 522 | + | (Err(_), None) => true, |
|
| 523 | + | (Ok(m), Some(f)) if m.is_file() && f.kind == NodeKind::File => { |
|
| 524 | + | if is_executable(&m) != f.mode.exec { |
|
| 525 | + | return false; |
|
| 526 | + | } |
|
| 527 | + | let want = match &f.content { |
|
| 528 | + | FileContent::Text { bytes, .. } => Some(bytes.clone()), |
|
| 529 | + | FileContent::Opaque { blobs } if blobs.len() == 1 => { |
|
| 530 | + | self.read_blob(&blobs[0]).ok() |
|
| 531 | + | } |
|
| 532 | + | _ => None, |
|
| 533 | + | }; |
|
| 534 | + | want.is_some_and(|w| fs::read(&full).is_ok_and(|have| have == w)) |
|
| 535 | + | } |
|
| 536 | + | _ => false, |
|
| 537 | + | } |
|
| 538 | + | }; |
|
| 539 | + | touched.retain(|p| same(p) || !already(p)); |
|
| 515 | 540 | let differ: Vec<&String> = touched.iter().filter(|p| !same(p)).collect(); |
|
| 516 | 541 | if !differ.is_empty() { |
|
| 517 | 542 | let list: Vec<&str> = differ.iter().take(10).map(|p| p.as_str()).collect(); |
|
| … | 159 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +47 −11
| … | 5243 unchanged lines |
||
| 5244 | 5244 | err.contains("modified a.txt") && err.contains("removed b.txt") && err.contains("--carry"), |
|
| 5245 | 5245 | "{err}" |
|
| 5246 | 5246 | ); |
|
| 5247 | - | // --carry takes them along: h1 has a.txt as the base has it, and no b.txt |
|
| 5248 | - | // is carried as absent (h1 has none either, so that is refused). |
|
| 5249 | - | let err = repo |
|
| 5250 | - | .checkout_carry(&[h1], "the first change", "checkout") |
|
| 5251 | - | .unwrap_err() |
|
| 5252 | - | .to_string(); |
|
| 5253 | - | assert!( |
|
| 5254 | - | err.contains("b.txt") && err.contains("the first change"), |
|
| 5255 | - | "{err}" |
|
| 5256 | - | ); |
|
| 5257 | - | write(root, "b.txt", b"b\n"); |
|
| 5247 | + | // --carry takes them along: h1 has a.txt as the base has it, and the |
|
| 5248 | + | // removed b.txt is already as h1 has it (none), so that is no conflict |
|
| 5249 | + | // either (oxygen#100). |
|
| 5258 | 5250 | let (_, carried) = repo |
|
| 5259 | 5251 | .checkout_carry(&[h1], "the first change", "checkout") |
|
| 5260 | 5252 | .unwrap(); |
|
| … | 3257 unchanged lines |
||
| 8518 | 8510 | b"a\n", |
|
| 8519 | 8511 | "committed files stay" |
|
| 8520 | 8512 | ); |
|
| 8513 | + | } |
|
| 8514 | + | ||
| 8515 | + | /// oxygen#100: uncommitted work byte-for-byte what the bookmark switched to |
|
| 8516 | + | /// already has is no conflict: the switch goes through, nothing is lost, |
|
| 8517 | + | /// and other carried work still comes along. |
|
| 8518 | + | #[test] |
|
| 8519 | + | fn carrying_work_the_target_already_has_is_no_conflict() { |
|
| 8520 | + | hermetic(); |
|
| 8521 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 8522 | + | let root = dir.path(); |
|
| 8523 | + | let mut repo = Repo::init(root).unwrap(); |
|
| 8524 | + | write(root, "a.txt", b"one\n"); |
|
| 8525 | + | write(root, "b.txt", b"b\n"); |
|
| 8526 | + | repo.commit("one", &[]).unwrap(); |
|
| 8527 | + | repo.bookmark_create("landed", None).unwrap(); |
|
| 8528 | + | // On landed, a.txt becomes "two" (as a proposal landing would). |
|
| 8529 | + | repo.bookmark_switch("landed", false).unwrap(); |
|
| 8530 | + | write(root, "a.txt", b"two\n"); |
|
| 8531 | + | repo.commit("two", &[]).unwrap(); |
|
| 8532 | + | repo.bookmark_switch("main", false).unwrap(); |
|
| 8533 | + | assert_eq!(std::fs::read(root.join("a.txt")).unwrap(), b"one\n"); |
|
| 8534 | + | // The same edit, uncommitted, on main; and another edit besides. |
|
| 8535 | + | write(root, "a.txt", b"two\n"); |
|
| 8536 | + | write(root, "b.txt", b"b, edited\n"); |
|
| 8537 | + | let (_, carried) = repo.bookmark_switch_carry("landed").unwrap(); |
|
| 8538 | + | assert_eq!( |
|
| 8539 | + | carried, |
|
| 8540 | + | vec!["b.txt".to_string()], |
|
| 8541 | + | "only the work landed lacks is carried" |
|
| 8542 | + | ); |
|
| 8543 | + | assert_eq!(std::fs::read(root.join("a.txt")).unwrap(), b"two\n"); |
|
| 8544 | + | assert_eq!(std::fs::read(root.join("b.txt")).unwrap(), b"b, edited\n"); |
|
| 8545 | + | let s = repo.snapshot("oxy status").unwrap(); |
|
| 8546 | + | let paths: Vec<_> = s.statuses.iter().map(|p| p.path.as_str()).collect(); |
|
| 8547 | + | assert_eq!(paths, ["b.txt"]); |
|
| 8548 | + | ||
| 8549 | + | // A real difference is still refused. |
|
| 8550 | + | repo.bookmark_switch("main", true).unwrap(); |
|
| 8551 | + | write(root, "a.txt", b"three\n"); |
|
| 8552 | + | let e = repo |
|
| 8553 | + | .bookmark_switch_carry("landed") |
|
| 8554 | + | .unwrap_err() |
|
| 8555 | + | .to_string(); |
|
| 8556 | + | assert!(e.contains("a.txt") && e.contains("--discard"), "{e}"); |
|
| 8521 | 8557 | } |