use axum::{Json, extract::State, response::IntoResponse};
use serde::Serialize;
use super::AppState;
use super::report_bug::{TargetSource, resolve_target};
const PUBLIC_GIT_HOSTS: &[&str] = &["github.com", "gitlab.com", "bitbucket.org"];
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Connectivity {
pub(crate) offline: bool,
pub(crate) repo_url: String,
pub(crate) repo_reachable: bool,
}
fn parse_override(s: &str) -> Option<bool> {
match s.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "offline" | "airgap" | "air-gapped" => Some(true),
"0" | "false" | "no" | "off" | "online" => Some(false),
_ => None,
}
}
fn host_of(web_url: &str) -> Option<String> {
let after = web_url.split_once("://").map_or(web_url, |(_, rest)| rest);
let host = after.split(['/', ':']).next()?;
if host.is_empty() {
None
} else {
Some(host.to_ascii_lowercase())
}
}
fn is_public_host(host: &str) -> bool {
PUBLIC_GIT_HOSTS
.iter()
.any(|p| host == *p || host.ends_with(&format!(".{p}")))
}
fn decide_offline(explicit: Option<bool>, origin_host: Option<&str>, is_default: bool) -> bool {
if let Some(forced) = explicit {
return forced;
}
if !is_default && let Some(host) = origin_host {
return !is_public_host(host);
}
false
}
pub(crate) fn detect(state: &AppState) -> Connectivity {
let explicit = std::env::var("SLOC_AIRGAP")
.ok()
.as_deref()
.and_then(parse_override)
.or(state.base_config.reporting.offline_mode);
let target = resolve_target(state);
let is_default = target.source() == TargetSource::Default;
let host = host_of(target.web_url());
let offline = decide_offline(explicit, host.as_deref(), is_default);
let repo_reachable = !is_default && host.as_deref().is_some_and(|h| !is_public_host(h));
Connectivity {
offline,
repo_url: target.web_url().to_string(),
repo_reachable,
}
}
pub(crate) async fn connectivity_handler(State(state): State<AppState>) -> impl IntoResponse {
Json(detect(&state))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_wins_over_heuristic() {
assert!(decide_offline(Some(true), Some("github.com"), false));
assert!(!decide_offline(
Some(false),
Some("git.internal.corp"),
false
));
}
#[test]
fn public_origin_is_online() {
assert!(!decide_offline(None, Some("github.com"), false));
assert!(!decide_offline(None, Some("gitlab.com"), false));
assert!(!decide_offline(None, Some("bitbucket.org"), false));
}
#[test]
fn public_subdomain_is_online() {
assert!(!decide_offline(None, Some("www.github.com"), false));
}
#[test]
fn internal_origin_is_offline() {
assert!(decide_offline(None, Some("git.internal.corp"), false));
assert!(decide_offline(None, Some("bitbucket.instance2.com"), false));
}
#[test]
fn default_fallback_stays_online() {
assert!(!decide_offline(None, Some("github.com"), true));
assert!(!decide_offline(None, None, true));
}
#[test]
fn parse_override_variants() {
for t in ["1", "true", "YES", "on", "offline", "airgap", "air-gapped"] {
assert_eq!(parse_override(t), Some(true), "{t}");
}
for f in ["0", "false", "NO", "off", "online"] {
assert_eq!(parse_override(f), Some(false), "{f}");
}
for u in ["", " ", "maybe", "2"] {
assert_eq!(parse_override(u), None, "{u:?}");
}
}
#[test]
fn is_public_host_matches_hosts_and_subdomains() {
assert!(is_public_host("github.com"));
assert!(is_public_host("gitlab.com"));
assert!(is_public_host("bitbucket.org"));
assert!(is_public_host("www.github.com"));
assert!(!is_public_host("git.internal.corp"));
assert!(!is_public_host("github.com.evil.example")); }
#[test]
fn host_of_extracts_host() {
assert_eq!(
host_of("https://github.com/a/b").as_deref(),
Some("github.com")
);
assert_eq!(
host_of("http://gitlab.internal:2222/team/repo").as_deref(),
Some("gitlab.internal")
);
assert_eq!(host_of("").as_deref(), None);
}
}