OXYGENHUB

oh takes a hub and token from the checkout, and from a subrepo's parent cefb0ea24511

In a checkout whose remote has no token or CA (a subrepo cloned before
oxy recorded them), oh uses those of an enclosing checkout's remote for
the same hub before the saved ones. oh --repo OWNER/NAME inside a
checkout takes the checkout remote's hub, and its token and CA for that
hub, rather than asking which hub.

closes #50, closes #51
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 14:08 UTC
Signature
✔ signed by Andy McCall
Hash
cefb0ea24511445166d0db129929255276092d452c7832e877235b49fcdc5881
Depends on
5c6e92fd8d71
Depended on by
nothing yet
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
8 atoms, 0 tree operations, 1 file

Browse the tree at this change

modified crates/oh/src/target.rs +72 −4
…
101 unchanged lines
102 102
    }
103 103
}
104 104
105 +
/// A token and CA for `hub` from the checkout at `dir` or one enclosing
106 +
/// it (a subrepo's parent): the first remote on that hub that has each.
107 +
fn checkout_credentials(dir: &Path, hub: &str) -> (Option<String>, Option<PathBuf>) {
108 +
    let (mut token, mut ca) = (None, None);
109 +
    let mut at = Some(dir.to_path_buf());
110 +
    while let Some(d) = at {
111 +
        let Ok(repo) = oxy_core::repo::Repo::open(&d) else { break };
112 +
        for (name, url) in &repo.config.remotes {
113 +
            if split_url(url).is_ok_and(|(h, _, _)| h.eq_ignore_ascii_case(hub)) {
114 +
                token = token.or_else(|| repo.config.remote_tokens.get(name).cloned());
115 +
                ca = ca.or_else(|| repo.config.remote_cas.get(name).map(PathBuf::from));
116 +
            }
117 +
        }
118 +
        if token.is_some() && ca.is_some() {
119 +
            break;
120 +
        }
121 +
        at = repo.root().parent().map(Path::to_path_buf);
122 +
    }
123 +
    (token, ca)
124 +
}
125 +
126 +
/// The hub of the checkout's remote (the one chosen, `origin`, or the only
127 +
/// one), for `--repo` without `--hub`.
128 +
fn checkout_hub(dir: &Path, remote: Option<&str>) -> Option<String> {
129 +
    let repo = oxy_core::repo::Repo::open(dir).ok()?;
130 +
    let remotes = &repo.config.remotes;
131 +
    let url = match remote {
132 +
        Some(n) => remotes.get(n)?,
133 +
        None => remotes.get("origin").or_else(|| (remotes.len() == 1).then(|| remotes.values().next()).flatten())?,
134 +
    };
135 +
    split_url(url).ok().map(|(hub, _, _)| hub)
136 +
}
137 +
105 138
pub struct Choice<'a> {
106 139
    pub repo: Option<&'a str>,
107 140
    pub hub: Option<&'a str>,
…
8 unchanged lines
116 149
    let saved = |hub: &str| cfg.hosts.get(hub).cloned();
117 150
    if let Some(r) = choice.repo {
118 151
        let (owner, name) = r.split_once('/').ok_or_else(|| anyhow!("--repo takes OWNER/NAME"))?;
152 +
        // Inside a checkout, its remote's hub, token and CA serve for
153 +
        // another repository on the same hub.
119 154
        let hub = choice
120 155
            .hub
121 156
            .map(String::from)
122 157
            .or_else(|| std::env::var("OH_HUB").ok())
158 +
            .or_else(|| checkout_hub(dir, choice.remote))
123 159
            .or_else(|| cfg.default_hub.clone())
124 160
            .ok_or_else(|| anyhow!("which hub? give --hub URL, set OH_HUB, or run `oh auth login`"))?;
125 161
        let hub = hub.trim_end_matches('/').to_string();
126 162
        let host = saved(&hub).unwrap_or_default();
163 +
        let (co_token, co_ca) = checkout_credentials(dir, &hub);
127 164
        return Ok(Target {
128 -
            token: env_token.or(Some(host.token).filter(|t| !t.is_empty())),
129 -
            ca: env_ca.or(host.ca),
165 +
            token: env_token.or(co_token).or(Some(host.token).filter(|t| !t.is_empty())),
166 +
            ca: env_ca.or(co_ca).or(host.ca),
130 167
            hub,
131 168
            owner: owner.into(),
132 169
            name: name.into(),
…
15 unchanged lines
148 185
        let url = remotes.get(&name).ok_or_else(|| anyhow!("no remote named {name}"))?;
149 186
        let (hub, owner, repo_name) = split_url(url)?;
150 187
        let host = saved(&hub).unwrap_or_default();
151 -
        let token = env_token.or_else(|| repo.config.remote_tokens.get(&name).cloned()).or(Some(host.token).filter(|t| !t.is_empty()));
152 -
        let ca = env_ca.or_else(|| repo.config.remote_cas.get(&name).map(PathBuf::from)).or(host.ca);
188 +
        // The remote's own, then a parent checkout's for the same hub (a
189 +
        // subrepo cloned before it recorded them), then the saved ones.
190 +
        let (up_token, up_ca) = repo.root().parent().map(|p| checkout_credentials(p, &hub)).unwrap_or_default();
191 +
        let token =
192 +
            env_token.or_else(|| repo.config.remote_tokens.get(&name).cloned()).or(up_token).or(Some(host.token).filter(|t| !t.is_empty()));
193 +
        let ca = env_ca.or_else(|| repo.config.remote_cas.get(&name).map(PathBuf::from)).or(up_ca).or(host.ca);
153 194
        return Ok(Target { hub, token, ca, owner, name: repo_name });
154 195
    }
155 196
    // No repository: commands that need only a hub.
…
27 unchanged lines
183 224
        assert_eq!(split_url("http://127.0.0.1:9/a/b/").unwrap().2, "b");
184 225
        assert!(split_url("https://hub/only-owner").is_err());
185 226
        assert!(split_url("not a url").is_err());
227 +
    }
228 +
229 +
    /// oxygenhub#50, #51: a subrepo checkout whose remote has no token
230 +
    /// borrows its parent's for the same hub, and --repo inside a checkout
231 +
    /// takes the checkout's hub and token.
232 +
    #[test]
233 +
    fn checkouts_lend_their_hub_and_token() {
234 +
        if std::env::var_os("OH_TOKEN").is_some() || std::env::var_os("OXY_TOKEN").is_some() || std::env::var_os("OH_HUB").is_some() {
235 +
            return;
236 +
        }
237 +
        let dir = tempfile::tempdir().unwrap();
238 +
        let parent = dir.path().join("metalgear");
239 +
        let mut p = oxy_core::repo::Repo::init(&parent).unwrap();
240 +
        p.config.set("remote.origin.url", "https://hub.example/doomsdayone/metalgear").unwrap();
241 +
        p.config.set("remote.origin.token", "parent-token").unwrap();
242 +
        p.save_config().unwrap();
243 +
        let sub = parent.join("rstar-api");
244 +
        let mut s = oxy_core::repo::Repo::init(&sub).unwrap();
245 +
        s.config.set("remote.origin.url", "https://hub.example/doomsdayone/rstar-api").unwrap();
246 +
        s.save_config().unwrap();
247 +
        let cfg = Config::default();
248 +
        let none = Choice { repo: None, hub: None, remote: None };
249 +
        let t = resolve(&none, &sub, &cfg).unwrap();
250 +
        assert_eq!((t.name.as_str(), t.token.as_deref()), ("rstar-api", Some("parent-token")));
251 +
        let other = Choice { repo: Some("doomsdayone/other"), hub: None, remote: None };
252 +
        let t = resolve(&other, &parent, &cfg).unwrap();
253 +
        assert_eq!((t.hub.as_str(), t.token.as_deref()), ("https://hub.example", Some("parent-token")));
186 254
    }
187 255
}