use std::path::Path;
use serde::Serialize;
use crate::config::{CONFIG_NAME, Config};
use crate::dvc;
use crate::error::Result;
use crate::git::GitRepo;
use crate::manifest::{ResolvedTask, TaskKind};
use crate::process::command_exists;
use crate::scaffold;
#[derive(Debug, Clone, Serialize)]
pub struct DoctorReport {
pub status: String,
pub repo: String,
pub cli_version: String,
pub checks: Vec<DoctorCheck>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DoctorCheck {
pub name: String,
pub status: String,
pub detail: String,
}
impl DoctorReport {
pub fn healthy(&self) -> bool {
self.checks.iter().all(|check| check.status == "ok")
}
}
pub fn inspect(path: &Path) -> Result<DoctorReport> {
let repo = GitRepo::discover(path)?;
let mut checks = Vec::new();
let mut repository_runtime = command_check("git", true);
repository_runtime.name = "repository-runtime".to_owned();
checks.push(repository_runtime);
let config_path = repo.root.join(CONFIG_NAME);
let config = match Config::load_compatible(&repo) {
Ok(config) => {
checks.push(DoctorCheck {
name: "repository-config".to_owned(),
status: "ok".to_owned(),
detail: config_path.display().to_string(),
});
Some(config)
}
Err(error) => {
checks.push(DoctorCheck {
name: "repository-config".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
});
None
}
};
if let Some(config) = &config {
checks.push(match scaffold::validate_owned_files(&repo, config) {
Ok(()) => DoctorCheck {
name: "repository-scaffold".to_owned(),
status: "ok".to_owned(),
detail: "product-owned files match the installed CLI".to_owned(),
},
Err(error) => DoctorCheck {
name: "repository-scaffold".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
},
});
let head = repo
.current_branch()?
.unwrap_or_else(|| "detached".to_owned());
let expected = &config.git.branch;
let infrastructure_task = if head == *expected {
None
} else {
ResolvedTask::discover(&repo, &repo.root)
.and_then(|path| ResolvedTask::load(&repo, config, &path))
.ok()
.filter(|task| task.kind == TaskKind::Infrastructure && task.branch == head)
};
let branch_ok = head == *expected || infrastructure_task.is_some();
checks.push(DoctorCheck {
name: "checkout-branch".to_owned(),
status: if branch_ok { "ok" } else { "error" }.to_owned(),
detail: match infrastructure_task {
Some(task) => format!(
"current {head}, valid isolated infrastructure task {}",
task.task_id
),
None => format!("current {head}, configured shared branch {expected}"),
},
});
let identity = repo.run_unchecked(["var", "GIT_AUTHOR_IDENT"])?;
checks.push(DoctorCheck {
name: "publication-identity".to_owned(),
status: if identity.success() { "ok" } else { "error" }.to_owned(),
detail: if identity.success() {
"publication author name and email are configured".to_owned()
} else {
"publication author name or email is not configured".to_owned()
},
});
let remote_name_ok = repo.validate_remote_name(&config.git.remote).is_ok();
let remote = if remote_name_ok {
repo.run_unchecked(["remote", "get-url", "--", &config.git.remote])?
} else {
crate::process::CommandOutput {
code: 1,
stdout: String::new(),
stderr: "invalid configured remote name".to_owned(),
}
};
checks.push(DoctorCheck {
name: "publication-remote".to_owned(),
status: if remote.success() { "ok" } else { "error" }.to_owned(),
detail: if remote.success() {
format!("configured remote {:?} exists", config.git.remote)
} else {
format!("configured remote {:?} does not exist", config.git.remote)
},
});
if config.s3_enabled() {
checks.push(match dvc::require_runtime(&repo) {
Ok(version) => DoctorCheck {
name: "managed-storage-runtime".to_owned(),
status: "ok".to_owned(),
detail: format!("internal engine {version}"),
},
Err(error) => DoctorCheck {
name: "managed-storage-runtime".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
},
});
checks.push(match dvc::validate_internal_config(&repo, config) {
Ok(()) => DoctorCheck {
name: "managed-storage-config".to_owned(),
status: "ok".to_owned(),
detail: "internal configuration matches .workspace-mgr.toml".to_owned(),
},
Err(error) => DoctorCheck {
name: "managed-storage-config".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
},
});
if config.requires_object_versioning() {
checks.push(match dvc::require_version_adapter(&repo) {
Ok(adapter) => DoctorCheck {
name: "managed-storage-version-adapter".to_owned(),
status: "ok".to_owned(),
detail: adapter,
},
Err(error) => DoctorCheck {
name: "managed-storage-version-adapter".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
},
});
checks.push(match dvc::verify_object_versioning(&repo, config) {
Ok(detail) => DoctorCheck {
name: "managed-storage-object-versioning".to_owned(),
status: "ok".to_owned(),
detail: detail.to_string(),
},
Err(error) => DoctorCheck {
name: "managed-storage-object-versioning".to_owned(),
status: "error".to_owned(),
detail: error.to_string(),
},
});
}
let local = repo.root.join(".dvc/config.local");
if local.exists() {
let relative = ".dvc/config.local";
let ignored = repo.run_unchecked(["check-ignore", "--quiet", "--", relative])?;
checks.push(DoctorCheck {
name: "managed-storage-local-secrets".to_owned(),
status: if ignored.code == 0 { "ok" } else { "error" }.to_owned(),
detail: if ignored.code == 0 {
format!("{relative} is ignored")
} else {
format!("{relative} exists but is not ignored")
},
});
}
}
}
let status = if checks.iter().all(|check| check.status == "ok") {
"ok"
} else {
"error"
};
Ok(DoctorReport {
status: status.to_owned(),
repo: repo.root.display().to_string(),
cli_version: env!("CARGO_PKG_VERSION").to_owned(),
checks,
})
}
fn command_check(command: &str, required: bool) -> DoctorCheck {
let exists = command_exists(command);
DoctorCheck {
name: format!("command:{command}"),
status: if exists || !required { "ok" } else { "error" }.to_owned(),
detail: if exists {
which::which(command)
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "available".to_owned())
} else {
"not found on PATH".to_owned()
},
}
}