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 | } |