OXYGENHUB

Private subrepos clone with the parent's credentials for the same hub a07f60a37fb2

A subrepo on the same hub (scheme, host and port) as one of the parent's
remotes is probed and cloned with that remote's token and CA, which are
recorded on the subrepo's origin so it fetches, pushes and works with oh
afterwards; otherwise the environment's OXY_TOKEN and OXY_CA are used as
before and not recorded. So oxy submodule update --init of a private
subrepo works for anyone who can read the parent (metalgear feedback 23).

closes #32
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 14:06 UTC
Signature
✔ signed by Andy McCall
Hash
a07f60a37fb21005b17ecb3bcca6fb906c70307110b9211030ebaa9246bc8ada
Depends on
0955469ac0e1 2b5ee371060e 4dee277c327a 982864068a59 b3c5a59a875a d77e1702d770
Depended on by
d36ba2f79f95 9f9e72b93e4e ff1f62fee654 ce14f0d68b15
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
10 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/subrepos.rs +9 −14
…
107 unchanged lines
108 108
    Ok(!git_out(dir, &["status", "--porcelain"])?.is_empty())
109 109
}
110 110
111 -
/// Whether `url` names an Oxygen repository (else it is taken to be Git).
112 -
/// Whether `url` is an Oxygen repository, probed with the environment's
113 -
/// credentials (`OXY_TOKEN`, `OXY_CA`) as a clone would use them; with the
114 -
/// probe's error when it is not, to report if Git cannot clone it either.
115 -
fn probe_oxy_url(url: &str) -> std::result::Result<(), String> {
111 +
/// Whether `url` is an Oxygen repository (else it is taken to be Git),
112 +
/// probed with `options`, the credentials a clone of it would use; with
113 +
/// the probe's error when it is not, to report if Git cannot clone it
114 +
/// either.
115 +
fn probe_oxy_url(url: &str, options: &RemoteOptions) -> std::result::Result<(), String> {
116 116
    if url.starts_with("http://") || url.starts_with("https://") {
117 -
        let options = RemoteOptions {
118 -
            token: std::env::var("OXY_TOKEN").ok(),
119 -
            ca: std::env::var("OXY_CA").ok().map(std::path::PathBuf::from),
120 -
            repo_id: None,
121 -
        };
122 -
        return crate::protocol::open_remote(url, &options)
117 +
        return crate::protocol::open_remote(url, options)
123 118
            .and_then(|r| r.repo_id())
124 119
            .map(|_| ())
125 120
            .map_err(|e| e.to_string());
…
42 unchanged lines
168 163
            }
169 164
            let _ = fs::remove_dir(&dir);
170 165
            let r = match pin {
171 -
                Pin::Oxy { .. } => Repo::clone(&m.url, &dir).map(|_| ()),
166 +
                Pin::Oxy { .. } => self.clone_as_subrepo(&m.url, &dir).map(|_| ()),
172 167
                Pin::Git { .. } => Command::new("git")
173 168
                    .args(["clone", "-q", &m.url])
174 169
                    .arg(&dir)
…
152 unchanged lines
327 322
        if let Some(parent) = dir.parent() {
328 323
            fs::create_dir_all(parent)?;
329 324
        }
330 -
        let probe = probe_oxy_url(url);
325 +
        let probe = probe_oxy_url(url, &self.credentials_for(url));
331 326
        let git = probe.is_err();
332 327
        if git {
333 328
            let mut cmd = Command::new("git");
…
14 unchanged lines
348 343
            }
349 344
        } else {
350 345
            let _ = fs::remove_dir(&dir);
351 -
            let mut nested = Repo::clone(url, &dir)?;
346 +
            let mut nested = self.clone_as_subrepo(url, &dir)?;
352 347
            if let Some(b) = bookmark {
353 348
                nested.bookmark_switch(b, true)?;
354 349
            }
…
74 unchanged lines
modified crates/oxy-core/src/repo/sync.rs +69 −1
…
1684 unchanged lines
1685 1685
            ca: std::env::var("OXY_CA").ok().map(PathBuf::from),
1686 1686
            repo_id: None,
1687 1687
        };
1688 -
        let remote = open_remote(url, &options)?;
1688 +
        Self::clone_using(url, dest, max_blob, sparse, depth, &options, false)
1689 +
    }
1690 +
1691 +
    /// Clone a subrepo with credentials the parent has for the same hub
1692 +
    /// (see [`Repo::credentials_for`]), recording them on its `origin` so it
1693 +
    /// fetches and pushes with them after.
1694 +
    pub(crate) fn clone_as_subrepo(&self, url: &str, dest: &Path) -> Result<Repo> {
1695 +
        // Only credentials from a remote are recorded; the environment's
1696 +
        // stay in the environment.
1697 +
        let borrowed = self.remote_on_same_hub(url).is_some();
1698 +
        let options = self.credentials_for(url);
1699 +
        Self::clone_using(url, dest, None, &[], None, &options, borrowed)
1700 +
    }
1701 +
1702 +
    /// The name of a remote of this repository on the same hub (scheme,
1703 +
    /// host and port) as `url`.
1704 +
    fn remote_on_same_hub(&self, url: &str) -> Option<String> {
1705 +
        let origin = |u: &str| -> Option<String> {
1706 +
            let (scheme, rest) = u.split_once("://")?;
1707 +
            let host = rest.split('/').next()?;
1708 +
            Some(format!(
1709 +
                "{}://{}",
1710 +
                scheme.to_ascii_lowercase(),
1711 +
                host.to_ascii_lowercase()
1712 +
            ))
1713 +
        };
1714 +
        let want = origin(url)?;
1715 +
        self.config
1716 +
            .remotes
1717 +
            .iter()
1718 +
            .find(|(_, u)| origin(u).as_deref() == Some(want.as_str()))
1719 +
            .map(|(name, _)| name.clone())
1720 +
    }
1721 +
1722 +
    /// The token and CA to reach `url` with: those of a remote of this
1723 +
    /// repository on the same hub (scheme, host and port), else the
1724 +
    /// environment's (`OXY_TOKEN`, `OXY_CA`), as a clone would use them.
1725 +
    pub(crate) fn credentials_for(&self, url: &str) -> RemoteOptions {
1726 +
        match self.remote_on_same_hub(url) {
1727 +
            Some(name) => RemoteOptions {
1728 +
                repo_id: None,
1729 +
                ..self.remote_options(&name)
1730 +
            },
1731 +
            None => RemoteOptions {
1732 +
                token: std::env::var("OXY_TOKEN").ok(),
1733 +
                ca: std::env::var("OXY_CA").ok().map(PathBuf::from),
1734 +
                repo_id: None,
1735 +
            },
1736 +
        }
1737 +
    }
1738 +
1739 +
    fn clone_using(
1740 +
        url: &str,
1741 +
        dest: &Path,
1742 +
        max_blob: Option<u64>,
1743 +
        sparse: &[String],
1744 +
        depth: Option<u32>,
1745 +
        options: &RemoteOptions,
1746 +
        record: bool,
1747 +
    ) -> Result<Repo> {
1748 +
        let remote = open_remote(url, options)?;
1689 1749
        let id = remote.repo_id()?;
1690 1750
        if id == crate::protocol::UNBOUND_ID {
1691 1751
            return fail(format!(
…
2 unchanged lines
1694 1754
        }
1695 1755
        let mut repo = Repo::init_with_id(dest, id)?;
1696 1756
        repo.config.set("remote.origin.url", url)?;
1757 +
        if record {
1758 +
            if let Some(t) = &options.token {
1759 +
                repo.config.set("remote.origin.token", t)?;
1760 +
            }
1761 +
            if let Some(ca) = &options.ca {
1762 +
                repo.config.set("remote.origin.ca", &ca.to_string_lossy())?;
1763 +
            }
1764 +
        }
1697 1765
        if let Some(m) = max_blob {
1698 1766
            repo.config.set("filter.max-blob", &m.to_string())?;
1699 1767
        } else if !sparse.is_empty() {
…
40 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +31 −0
…
6198 unchanged lines
6199 6199
        rep.problems
6200 6200
    );
6201 6201
}
6202 +
6203 +
/// oxygen#32: a subrepo on the same hub as one of the parent's remotes is
6204 +
/// reached with that remote's token and CA (a private subrepo then clones
6205 +
/// for anyone who can read the parent); another hub gets none of them.
6206 +
#[test]
6207 +
fn a_subrepo_on_the_parents_hub_borrows_its_credentials() {
6208 +
    hermetic();
6209 +
    let dir = tempfile::tempdir().unwrap();
6210 +
    let mut repo = Repo::init(dir.path()).unwrap();
6211 +
    repo.config
6212 +
        .set(
6213 +
            "remote.origin.url",
6214 +
            "https://Hub.example:7343/doomsdayone/metalgear",
6215 +
        )
6216 +
        .unwrap();
6217 +
    repo.config.set("remote.origin.token", "secret").unwrap();
6218 +
    repo.config
6219 +
        .set("remote.origin.ca", "/tmp/hub-ca.pem")
6220 +
        .unwrap();
6221 +
    let same = repo.credentials_for("https://hub.example:7343/doomsdayone/rstar-api");
6222 +
    assert_eq!(same.token.as_deref(), Some("secret"));
6223 +
    assert_eq!(same.ca.as_deref(), Some(Path::new("/tmp/hub-ca.pem")));
6224 +
    for other in [
6225 +
        "https://elsewhere.example:7343/doomsdayone/rstar-api",
6226 +
        "https://hub.example/doomsdayone/rstar-api",
6227 +
        "http://hub.example:7343/doomsdayone/rstar-api",
6228 +
    ] {
6229 +
        let o = repo.credentials_for(other);
6230 +
        assert_ne!(o.token.as_deref(), Some("secret"), "{other}");
6231 +
    }
6232 +
}