submodule: name .oxymodules entries that were never added 7993d465675b
A .oxymodules section with no subrepo behind it got "No subrepos." and exit 0. status and update now list each such entry with the oxy submodule add command that pins it, update exits 1, and status --json lists them with a null pin. closes #72
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-05 18:22 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
7993d465675b26e4f5bbec0e278720f490531a3a1542e3969fbbc4ae50dd7e91- Depends on
- 14edbaaf5291 4dee277c327a 80677b7a3513 982864068a59 c6aa5bd9d1c8 e7ac6c2e84e4 ee06a6c557b7 fce44ae9ad29
- Depended on by
- bf3a67819b59
- 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.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
- 12 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified crates/oxy-core/src/repo/subrepos.rs +12 −0
| … | 304 unchanged lines |
||
| 305 | 305 | Ok(out) |
|
| 306 | 306 | } |
|
| 307 | 307 | ||
| 308 | + | /// `.oxymodules` entries with no subrepo behind them: never added, so |
|
| 309 | + | /// there is no pin to clone or check out (oxygen#72). |
|
| 310 | + | pub fn unpinned_modules(&self) -> Result<Vec<(String, Module)>> { |
|
| 311 | + | let pinned: Vec<String> = self.subrepos()?.into_iter().map(|s| s.path).collect(); |
|
| 312 | + | Ok(self |
|
| 313 | + | .modules_on_disk() |
|
| 314 | + | .entries |
|
| 315 | + | .into_iter() |
|
| 316 | + | .filter(|(path, _)| !pinned.contains(path)) |
|
| 317 | + | .collect()) |
|
| 318 | + | } |
|
| 319 | + | ||
| 308 | 320 | /// Clone `url` at `path`, register it in `.oxymodules`, and return the |
|
| 309 | 321 | /// pin the next snapshot will record. |
|
| 310 | 322 | pub fn submodule_add(&self, url: &str, path: &str, bookmark: Option<&str>) -> Result<Pin> { |
|
| … | 113 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +30 −0
| … | 7706 unchanged lines |
||
| 7707 | 7707 | "the new pin is committed" |
|
| 7708 | 7708 | ); |
|
| 7709 | 7709 | } |
|
| 7710 | + | ||
| 7711 | + | #[test] |
|
| 7712 | + | fn a_module_entry_that_was_never_added_is_reported_as_unpinned() { |
|
| 7713 | + | // oxygen#72: a .oxymodules section with no subrepo behind it. |
|
| 7714 | + | hermetic(); |
|
| 7715 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 7716 | + | let root = dir.path(); |
|
| 7717 | + | let _lib_origin = Repo::init(&root.join("lib-origin")).unwrap(); |
|
| 7718 | + | let lib_url = root.join("lib-origin").to_string_lossy().to_string(); |
|
| 7719 | + | let lib = Repo::clone(&lib_url, &root.join("lib")) |
|
| 7720 | + | .unwrap() |
|
| 7721 | + | .with_home(root.join("home")); |
|
| 7722 | + | write(&root.join("lib"), "lib.txt", b"v1\n"); |
|
| 7723 | + | lib.commit("lib v1", &[]).unwrap(); |
|
| 7724 | + | assert!(lib.push("origin", false).unwrap()[0].ok); |
|
| 7725 | + | let app_dir = root.join("app"); |
|
| 7726 | + | let app = Repo::init(&app_dir).unwrap().with_home(root.join("home")); |
|
| 7727 | + | write( |
|
| 7728 | + | &app_dir, |
|
| 7729 | + | ".oxymodules", |
|
| 7730 | + | format!("[libs/lib]\nurl = {lib_url}\n").as_bytes(), |
|
| 7731 | + | ); |
|
| 7732 | + | let unpinned = app.unpinned_modules().unwrap(); |
|
| 7733 | + | assert_eq!(unpinned.len(), 1); |
|
| 7734 | + | assert_eq!(unpinned[0].0, "libs/lib"); |
|
| 7735 | + | assert_eq!(unpinned[0].1.url, lib_url); |
|
| 7736 | + | assert!(app.submodule_update(true, &[]).unwrap().is_empty()); |
|
| 7737 | + | app.submodule_add(&lib_url, "libs/lib", None).unwrap(); |
|
| 7738 | + | assert!(app.unpinned_modules().unwrap().is_empty(), "added now"); |
|
| 7739 | + | } |
modified docs/guide.md +5 −2
| … | 521 unchanged lines |
||
| 522 | 522 | Committing the parent records whatever the nested repository is at, and |
|
| 523 | 523 | checking out an older parent state moves the nested repository back, unless |
|
| 524 | 524 | it has uncommitted changes, which are never overwritten. An absent nested |
|
| 525 | - | repository keeps its pin until you run `update --init`. Git submodules |
|
| 526 | - | survive `oxy git import` as Git pins and go back out as gitlinks on export. |
|
| 525 | + | repository keeps its pin until you run `update --init`. A `.oxymodules` |
|
| 526 | + | section written by hand has no pin until `oxy submodule add` clones it; |
|
| 527 | + | `status` and `update` name such entries, and `update` exits non-zero. Git |
|
| 528 | + | submodules survive `oxy git import` as Git pins and go back out as gitlinks |
|
| 529 | + | on export. |
|
| 527 | 530 | ||
| 528 | 531 | ## 12. Git interoperability |
|
| 529 | 532 | ||
| … | 117 unchanged lines |
modified src/cli/sync.rs +58 −19
| … | 710 unchanged lines |
||
| 711 | 711 | let repo = open()?; |
|
| 712 | 712 | let sp = ui::spinner("Updating subrepos…"); |
|
| 713 | 713 | let results = repo.submodule_update(init, paths)?; |
|
| 714 | + | let unpinned: Vec<_> = repo |
|
| 715 | + | .unpinned_modules()? |
|
| 716 | + | .into_iter() |
|
| 717 | + | .filter(|(p, _)| paths.is_empty() || paths.iter().any(|q| q.trim_end_matches('/') == p)) |
|
| 718 | + | .collect(); |
|
| 714 | 719 | sp.clear(); |
|
| 715 | - | Ok(print_subrepo_results(results)) |
|
| 720 | + | if results.is_empty() && unpinned.is_empty() { |
|
| 721 | + | println!("{}", ui::dim("No subrepos.")); |
|
| 722 | + | } |
|
| 723 | + | let code = print_subrepo_results(results); |
|
| 724 | + | print_unpinned(&unpinned); |
|
| 725 | + | Ok(if unpinned.is_empty() { code } else { 1 }) |
|
| 716 | 726 | } |
|
| 717 | 727 | ||
| 728 | + | /// `.oxymodules` entries never added: nothing to clone, so say how to add |
|
| 729 | + | /// them (oxygen#72). |
|
| 730 | + | fn print_unpinned(unpinned: &[(String, oxy_core::subrepo::Module)]) { |
|
| 731 | + | for (path, m) in unpinned { |
|
| 732 | + | let url = if m.url.is_empty() { "<url>" } else { &m.url }; |
|
| 733 | + | println!( |
|
| 734 | + | "{} {}", |
|
| 735 | + | ui::warn(format!( |
|
| 736 | + | "{} in .oxymodules but never added, so no pin", |
|
| 737 | + | ui::glyph("⚠", "!") |
|
| 738 | + | )), |
|
| 739 | + | ui::path(path) |
|
| 740 | + | ); |
|
| 741 | + | println!( |
|
| 742 | + | " {}", |
|
| 743 | + | ui::dim(format!("add it with: oxy submodule add {url} {path}")) |
|
| 744 | + | ); |
|
| 745 | + | } |
|
| 746 | + | } |
|
| 747 | + | ||
| 718 | 748 | fn print_subrepo_results(results: Vec<(String, oxy_core::repo::SubrepoSync)>) -> i32 { |
|
| 719 | - | if results.is_empty() { |
|
| 720 | - | println!("{}", ui::dim("No subrepos.")); |
|
| 721 | - | } |
|
| 722 | 749 | let mut code = 0; |
|
| 723 | 750 | for (path, r) in results { |
|
| 724 | 751 | use oxy_core::repo::SubrepoSync::*; |
|
| … | 55 unchanged lines |
||
| 780 | 807 | pub fn submodule_status(as_json: bool) -> Result<i32> { |
|
| 781 | 808 | let repo = open()?; |
|
| 782 | 809 | let subs = repo.subrepos()?; |
|
| 810 | + | let unpinned = repo.unpinned_modules()?; |
|
| 783 | 811 | if as_json { |
|
| 784 | - | println!( |
|
| 785 | - | "{}", |
|
| 786 | - | json::arr(subs.iter().map(|s| { |
|
| 787 | - | json::obj([ |
|
| 788 | - | ("path", json::s(&s.path)), |
|
| 789 | - | ("pin", pin_json(&s.pin)), |
|
| 790 | - | ("actual", json::opt(s.actual.as_ref(), pin_json)), |
|
| 791 | - | ("at_pin", json::b(s.actual.as_ref() == Some(&s.pin))), |
|
| 792 | - | ("dirty", json::b(s.dirty)), |
|
| 793 | - | ("committed", json::b(s.committed)), |
|
| 794 | - | ("url", json::opt(s.url.as_deref(), json::s)), |
|
| 795 | - | ]) |
|
| 796 | - | })) |
|
| 797 | - | ); |
|
| 812 | + | let pinned = subs.iter().map(|s| { |
|
| 813 | + | json::obj([ |
|
| 814 | + | ("path", json::s(&s.path)), |
|
| 815 | + | ("pin", pin_json(&s.pin)), |
|
| 816 | + | ("actual", json::opt(s.actual.as_ref(), pin_json)), |
|
| 817 | + | ("at_pin", json::b(s.actual.as_ref() == Some(&s.pin))), |
|
| 818 | + | ("dirty", json::b(s.dirty)), |
|
| 819 | + | ("committed", json::b(s.committed)), |
|
| 820 | + | ("url", json::opt(s.url.as_deref(), json::s)), |
|
| 821 | + | ]) |
|
| 822 | + | }); |
|
| 823 | + | // Listed in .oxymodules but never added: no pin at all. |
|
| 824 | + | let unpinned_json = unpinned.iter().map(|(path, m)| { |
|
| 825 | + | json::obj([ |
|
| 826 | + | ("path", json::s(path)), |
|
| 827 | + | ("pin", "null".to_string()), |
|
| 828 | + | ("actual", "null".to_string()), |
|
| 829 | + | ("at_pin", json::b(false)), |
|
| 830 | + | ("dirty", json::b(false)), |
|
| 831 | + | ("committed", json::b(false)), |
|
| 832 | + | ("url", json::s(&m.url)), |
|
| 833 | + | ]) |
|
| 834 | + | }); |
|
| 835 | + | println!("{}", json::arr(pinned.chain(unpinned_json))); |
|
| 798 | 836 | return Ok(0); |
|
| 799 | 837 | } |
|
| 800 | - | if subs.is_empty() { |
|
| 838 | + | if subs.is_empty() && unpinned.is_empty() { |
|
| 801 | 839 | println!("{}", ui::dim("No subrepos.")); |
|
| 802 | 840 | } |
|
| 803 | 841 | for s in subs { |
|
| … | 26 unchanged lines |
||
| 830 | 868 | println!(" {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u)); |
|
| 831 | 869 | } |
|
| 832 | 870 | } |
|
| 871 | + | print_unpinned(&unpinned); |
|
| 833 | 872 | Ok(0) |
|
| 834 | 873 | } |
|
| 835 | 874 | ||
| … | 35 unchanged lines |