OXYGENHUB

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