A hosted change can move a subrepo's pin f2f91ceb0e9f
Repo::build_pin_change builds a change that moves the subrepo at a path to a new pin, leaving everything else as it is; the path must already be a subrepo. The hub uses it to move a consumer's pin for Diffusion (oxygenhub#148). closes #109
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 00:41 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
f2f91ceb0e9f3b142481ddf2e851fa037beb14cc00a186b7cfe61bf45b030ba4- Depends on
- 84b9e9ac0ab4 982864068a59 aa4939e07a09 eae595568832 f7510b63cceb
- Depended on by
- 767ac6cd0545
- In bookmarks
- carried-not-removed 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
- 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
- 7 atoms, 0 tree operations, 2 files
Browse the tree at this change
modified crates/oxy-core/src/repo/hosted.rs +46 −1
| … | 25 unchanged lines |
||
| 26 | 26 | message: &str, |
|
| 27 | 27 | author: KeyId, |
|
| 28 | 28 | ) -> Result<Change> { |
|
| 29 | + | self.build_change_with(heads, edits, &[], message, author) |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | /// Build (but do not store or sign) a change that moves the subrepo at |
|
| 33 | + | /// `path` in the state with `heads` to `pin`: a hub moving a consumer's |
|
| 34 | + | /// pin for Diffusion (oxygenhub#148). The path must already be a |
|
| 35 | + | /// subrepo; nothing else changes. |
|
| 36 | + | pub fn build_pin_change( |
|
| 37 | + | &self, |
|
| 38 | + | heads: &[ChangeHash], |
|
| 39 | + | path: &str, |
|
| 40 | + | pin: &crate::subrepo::Pin, |
|
| 41 | + | message: &str, |
|
| 42 | + | author: KeyId, |
|
| 43 | + | ) -> Result<Change> { |
|
| 44 | + | self.build_change_with( |
|
| 45 | + | heads, |
|
| 46 | + | &[], |
|
| 47 | + | &[(path.to_string(), pin.clone())], |
|
| 48 | + | message, |
|
| 49 | + | author, |
|
| 50 | + | ) |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | fn build_change_with( |
|
| 54 | + | &self, |
|
| 55 | + | heads: &[ChangeHash], |
|
| 56 | + | edits: &[FileEdit], |
|
| 57 | + | pins: &[(String, crate::subrepo::Pin)], |
|
| 58 | + | message: &str, |
|
| 59 | + | author: KeyId, |
|
| 60 | + | ) -> Result<Change> { |
|
| 29 | 61 | if message.trim().is_empty() { |
|
| 30 | 62 | return fail("a change message is required"); |
|
| 31 | 63 | } |
|
| 32 | - | if edits.is_empty() { |
|
| 64 | + | if edits.is_empty() && pins.is_empty() { |
|
| 33 | 65 | return fail("nothing to change"); |
|
| 34 | 66 | } |
|
| 35 | 67 | let base = self.load_state(heads)?; |
|
| … | 59 unchanged lines |
||
| 95 | 127 | } |
|
| 96 | 128 | } |
|
| 97 | 129 | } |
|
| 130 | + | } |
|
| 131 | + | } |
|
| 132 | + | for (path, pin) in pins { |
|
| 133 | + | let path = path.trim_matches('/'); |
|
| 134 | + | match base_snapshot.files.get(path) { |
|
| 135 | + | Some(f) if f.kind == NodeKind::Subrepo => {} |
|
| 136 | + | Some(_) => return fail(format!("{path} is not a subrepo")), |
|
| 137 | + | None => return fail(format!("{path} does not exist")), |
|
| 98 | 138 | } |
|
| 139 | + | entries.insert( |
|
| 140 | + | path.to_string(), |
|
| 141 | + | FsEntry::new(NodeKind::Subrepo, false, pin.encode()), |
|
| 142 | + | ); |
|
| 99 | 143 | } |
|
| 100 | 144 | // A path the user writes settles its replace conflict, if any: they |
|
| 101 | 145 | // chose that content (spec §9.3). |
|
| … | 1 unchanged lines |
||
| 103 | 147 | .iter() |
|
| 104 | 148 | .filter(|e| e.content.is_some()) |
|
| 105 | 149 | .map(|e| e.path.trim_matches('/').to_string()) |
|
| 150 | + | .chain(pins.iter().map(|(p, _)| p.trim_matches('/').to_string())) |
|
| 106 | 151 | .collect(); |
|
| 107 | 152 | let built = self.build_from_entries_resolving( |
|
| 108 | 153 | &base, |
|
| … | 651 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +56 −0
| … | 8537 unchanged lines |
||
| 8538 | 8538 | .to_string() |
|
| 8539 | 8539 | .contains("no change matches")); |
|
| 8540 | 8540 | } |
|
| 8541 | + | ||
| 8542 | + | #[test] |
|
| 8543 | + | fn a_pin_change_moves_only_the_pin() { |
|
| 8544 | + | hermetic(); |
|
| 8545 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 8546 | + | let root = dir.path(); |
|
| 8547 | + | let _lib_origin = Repo::init(&root.join("lib-origin")).unwrap(); |
|
| 8548 | + | let lib_url = root.join("lib-origin").to_string_lossy().to_string(); |
|
| 8549 | + | let lib = Repo::clone(&lib_url, &root.join("lib")) |
|
| 8550 | + | .unwrap() |
|
| 8551 | + | .with_home(root.join("home")); |
|
| 8552 | + | write(&root.join("lib"), "lib.txt", b"v1\n"); |
|
| 8553 | + | lib.commit("lib v1", &[]).unwrap(); |
|
| 8554 | + | assert!(lib.push("origin", false).unwrap()[0].ok); |
|
| 8555 | + | let _app_origin = Repo::init(&root.join("app-origin")).unwrap(); |
|
| 8556 | + | let app_dir = root.join("app"); |
|
| 8557 | + | let app = Repo::clone(&root.join("app-origin").to_string_lossy(), &app_dir) |
|
| 8558 | + | .unwrap() |
|
| 8559 | + | .with_home(root.join("home")); |
|
| 8560 | + | write(&app_dir, "main.txt", b"app\n"); |
|
| 8561 | + | let pin1 = app.submodule_add(&lib_url, "libs/lib", None).unwrap(); |
|
| 8562 | + | let a1 = app.commit("app with lib", &[]).unwrap(); |
|
| 8563 | + | // The library moves on. |
|
| 8564 | + | write(&root.join("lib"), "more.txt", b"v2\n"); |
|
| 8565 | + | let l2 = lib.commit("lib v2", &[]).unwrap(); |
|
| 8566 | + | let crate::subrepo::Pin::Oxy { repo, bookmark, .. } = pin1 else { |
|
| 8567 | + | panic!("an Oxygen pin") |
|
| 8568 | + | }; |
|
| 8569 | + | let pin2 = crate::subrepo::Pin::Oxy { |
|
| 8570 | + | repo, |
|
| 8571 | + | heads: vec![l2], |
|
| 8572 | + | bookmark, |
|
| 8573 | + | }; |
|
| 8574 | + | let author = app.store().get_change(&a1).unwrap().author; |
|
| 8575 | + | let change = app |
|
| 8576 | + | .build_pin_change(&[a1], "libs/lib", &pin2, "Move lib to v2", author) |
|
| 8577 | + | .unwrap(); |
|
| 8578 | + | let mut state = app.load_state(&[a1]).unwrap(); |
|
| 8579 | + | state.add(change).unwrap(); |
|
| 8580 | + | let files = state.materialize().files; |
|
| 8581 | + | assert_eq!(files["libs/lib"].kind, NodeKind::Subrepo); |
|
| 8582 | + | let FileContent::Opaque { blobs } = &files["libs/lib"].content else { |
|
| 8583 | + | panic!("a pin is opaque") |
|
| 8584 | + | }; |
|
| 8585 | + | let read = crate::subrepo::Pin::decode(&app.read_blob(&blobs[0]).unwrap()).unwrap(); |
|
| 8586 | + | assert_eq!(read, pin2, "the new pin"); |
|
| 8587 | + | assert!( |
|
| 8588 | + | matches!(&files["main.txt"].content, FileContent::Text { bytes, .. } if bytes.as_slice() == b"app\n"), |
|
| 8589 | + | "nothing else changes" |
|
| 8590 | + | ); |
|
| 8591 | + | // Only a subrepo's pin can be moved this way. |
|
| 8592 | + | let e = app.build_pin_change(&[a1], "main.txt", &pin2, "x", author); |
|
| 8593 | + | assert!(format!("{:?}", e.unwrap_err()).contains("not a subrepo")); |
|
| 8594 | + | let e = app.build_pin_change(&[a1], "libs/none", &pin2, "x", author); |
|
| 8595 | + | assert!(format!("{:?}", e.unwrap_err()).contains("does not exist")); |
|
| 8596 | + | } |