-
Notifications
You must be signed in to change notification settings - Fork 37
fix(gl): make doctor diagnose the node the git transport will use #394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,9 @@ use anyhow::Result; | |
| use clap::Args; | ||
| use std::path::PathBuf; | ||
|
|
||
| use crate::http::NodeClient; | ||
| use gitlawb_core::resolve_transport_node; | ||
|
|
||
| use crate::http::{sanitize_node_msg, NodeClient}; | ||
|
|
||
| const PUBLIC_NODE: &str = "https://node.gitlawb.com"; | ||
| const GITHUB_API_BASE: &str = "https://api.github.com"; | ||
|
|
@@ -141,27 +143,51 @@ pub async fn run(args: DoctorArgs) -> Result<()> { | |
| } | ||
|
|
||
| // ── 3. GITLAWB_NODE env var ─────────────────────────────────────────── | ||
| match std::env::var("GITLAWB_NODE") { | ||
| // A loopback host is a legitimate setup (self-hosted node, dev | ||
| // harness) — the connectivity check below fails loudly if it is not | ||
| // actually reachable, so don't red-flag the configuration itself. | ||
| Ok(v) if is_loopback_url(&v) => { | ||
| checks.push(Check::pass( | ||
| "GITLAWB_NODE", | ||
| format!( | ||
| "{v} (local node — intentional for self-hosting/dev; unset to target the public network)" | ||
| ), | ||
| )); | ||
| } | ||
| Ok(v) if !v.is_empty() => { | ||
| checks.push(Check::pass("GITLAWB_NODE", v.to_string())); | ||
| } | ||
| _ => { | ||
| checks.push(Check::fail( | ||
| "GITLAWB_NODE", | ||
| "not set — git-remote-gitlawb will fall back to http://127.0.0.1:7545", | ||
| "export GITLAWB_NODE=https://node.gitlawb.com", | ||
| )); | ||
| // The node `git clone` / `git push` will contact is not always the node `gl` | ||
| // contacts: `--node` defaults to the public node and an explicit flag outranks | ||
| // the environment, so the two diverge whenever the variable is unset, blank, or | ||
| // overridden. Probing only `gl`'s node is what let doctor greenlight an install | ||
| // whose transport was dead, so resolve the transport's node through the same | ||
| // function the helper uses and report on that one. | ||
| let env_node = std::env::var("GITLAWB_NODE").ok(); | ||
| let transport_node = resolve_transport_node(env_node.as_deref()); | ||
| let gl_node = sanitize_node_msg(&args.node); | ||
|
|
||
| if transport_node == args.node { | ||
| // gl and the transport agree, so one row covers both. A loopback host is a | ||
| // legitimate setup (self-hosted node, dev harness) and the connectivity | ||
| // check below fails loudly if it is not actually reachable, so don't | ||
| // red-flag the configuration itself. | ||
| let detail = if is_loopback_url(&transport_node) { | ||
| format!( | ||
| "{gl_node} (local node, intentional for self-hosting/dev; unset to target the public network)" | ||
| ) | ||
| } else { | ||
| gl_node.clone() | ||
| }; | ||
| checks.push(Check::pass("GITLAWB_NODE", detail)); | ||
| } else { | ||
| let verdict = probe_transport(&transport_node).await; | ||
| let detail = format!( | ||
| "git push/clone will use {} ({}); gl targets {gl_node}", | ||
| sanitize_node_msg(&transport_node), | ||
| verdict.detail | ||
| ); | ||
| // Single-quote the value: this line is printed under "Suggested fixes" for | ||
| // the user to paste into a shell, and the URL is caller-supplied. | ||
| let fix = format!("export GITLAWB_NODE='{gl_node}'"); | ||
| // Tiering follows who chose the broken value. An unset variable on a stock | ||
| // install is advisory: `gl` works, only `git push`/`git clone` do not, and | ||
| // #357 requires that install to keep exiting 0. A variable the user set to | ||
| // something unusable is a real misconfiguration. | ||
| let env_was_set = env_node | ||
| .as_deref() | ||
| .map(str::trim) | ||
| .is_some_and(|v| !v.is_empty()); | ||
| if verdict.usable || !env_was_set { | ||
| checks.push(Check::warn("GITLAWB_NODE", detail, fix)); | ||
| } else { | ||
| checks.push(Check::fail("GITLAWB_NODE", detail, fix)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -319,6 +345,63 @@ pub async fn run(args: DoctorArgs) -> Result<()> { | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Outcome of probing the node the git transport will use. | ||
| struct TransportVerdict { | ||
| /// True only when the endpoint identified itself as a gitlawb node. A bare 200 | ||
| /// is not enough: any local service answering on that port would otherwise be | ||
| /// reported as a working transport, which is the false green this check exists | ||
| /// to catch. | ||
| usable: bool, | ||
| detail: String, | ||
| } | ||
|
|
||
| /// Probe `url` and describe what is actually there. | ||
| /// | ||
| /// Deliberately not `NodeClient`: this call must not follow the user's proxy | ||
| /// (a proxy swallows a loopback probe and reports a running local node as dead) | ||
| /// and must not inherit the 30s request timeout, which stalls every stock | ||
| /// `gl doctor` run behind a hung listener. | ||
| async fn probe_transport(url: &str) -> TransportVerdict { | ||
| let client = match reqwest::Client::builder() | ||
| .no_proxy() | ||
| .timeout(std::time::Duration::from_secs(3)) | ||
| .build() | ||
|
Comment on lines
+365
to
+368
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository conventions ---'
for f in /tmp/coderabbit-repo-knowledge/gitlawb-node-7dd62aa5/*/*.md; do
case "$f" in
*learnings*|*architecture*) continue ;;
esac
printf '\n### %s\n' "$f"
cat "$f"
done
printf '%s\n' '--- doctor structure and relevant ranges ---'
ast-grep outline crates/gl/src/doctor.rs
sed -n '330,420p' crates/gl/src/doctor.rs
printf '%s\n' '--- redirect policy definitions and uses ---'
rg -n -C 8 'redirect|Policy::|same.origin|same_origin|GITLAWB_NODE|doctor' crates/gl crates/git-remote-gitlawbRepository: Gitlawb/node Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- redirect-related source ---'
rg -n -C 12 'redirect|Policy::|same.origin|same_origin' crates/gitlawb-core crates/git-remote-gitlawb crates/gl --glob '*.rs' --glob '*.toml'
printf '%s\n' '--- doctor tests and imports ---'
sed -n '1,35p' crates/gl/src/doctor.rs
sed -n '560,760p' crates/gl/src/doctor.rs
printf '%s\n' '--- dependency declarations ---'
rg -n -C 4 'reqwest|gitlawb-core' Cargo.toml crates/*/Cargo.tomlRepository: Gitlawb/node Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- gl HTTP client redirect implementation ---'
sed -n '1,95p' crates/gl/src/http.rs
printf '%s\n' '--- doctor imports, probe callers, and tests ---'
sed -n '1,110p' crates/gl/src/doctor.rs
sed -n '330,410p' crates/gl/src/doctor.rs
sed -n '560,730p' crates/gl/src/doctor.rs
printf '%s\n' '--- reqwest workspace version ---'
rg -n -C 3 '^reqwest\s*=|reqwest' Cargo.toml crates/gl/Cargo.tomlRepository: Gitlawb/node Length of output: 17960 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- doctor transport-node flow ---'
sed -n '105,205p' crates/gl/src/doctor.rs
printf '%s\n' '--- resolved transport node contract ---'
sed -n '20,90p' crates/gitlawb-core/src/lib.rs
rg -n -C 8 'resolve_transport_node|probe_transport' crates/gl/src/doctor.rs crates/gitlawb-core/src/lib.rs
printf '%s\n' '--- locked reqwest version ---'
rg -n -A5 -B2 '^name = "reqwest"$' Cargo.lockRepository: Gitlawb/node Length of output: 14087 Use the transport redirect policy for the probe.
🤖 Prompt for AI Agents |
||
| { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| return TransportVerdict { | ||
| usable: false, | ||
| detail: format!("probe failed: {}", sanitize_node_msg(&e.to_string())), | ||
| } | ||
| } | ||
| }; | ||
| let target = format!("{}/", url.trim_end_matches('/')); | ||
| match client.get(&target).send().await { | ||
| Ok(resp) if resp.status().is_success() => { | ||
| let info = resp.json::<serde_json::Value>().await.unwrap_or_default(); | ||
| if info["did"].as_str().is_some() { | ||
| TransportVerdict { | ||
| usable: true, | ||
| detail: "reachable".to_string(), | ||
| } | ||
| } else { | ||
| TransportVerdict { | ||
| usable: false, | ||
| detail: "something is listening but it is not a gitlawb node".to_string(), | ||
| } | ||
| } | ||
| } | ||
| Ok(resp) => TransportVerdict { | ||
| usable: false, | ||
| detail: format!("returned HTTP {}", resp.status()), | ||
| }, | ||
| Err(e) => TransportVerdict { | ||
| usable: false, | ||
| detail: format!("unreachable: {}", sanitize_node_msg(&e.to_string())), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| /// Check if a binary name exists anywhere on PATH. | ||
| /// True when the rc file contains a real `unalias` command naming `gl` — | ||
| /// not a comment, and not a longer word like `unalias global`. Ordering | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Escape single quotes in the shell remedy.
sanitize_node_msgretains'. A node value containing'closes this assignment when a user pastes the displayed remedy. Escape single quotes before formatting the value.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents