use std::path::Path;
use anyhow::Result;
use crate::config::{Global, Hooks, Repo};
use crate::context::Context;
use crate::discover;
use crate::style;
pub fn run(ctx: &Context) -> Result<()> {
let mut report = Report::default();
let global = ctx.global().unwrap_or_else(|e| {
report.problem(format!("global config: {e:#}"));
Global::defaults()
});
let repo = ctx.repo().ok();
let repo_config = ctx
.repo_config()
.map_err(|e| report.problem(format!("repo config: {e:#}")))
.ok()
.flatten();
section_repo(repo.as_deref());
section_global(&global, &mut report);
section_repo_config(repo_config.as_ref(), &mut report);
if !global.projects_roots.is_empty() {
section_discovered(&global, &mut report);
}
report.emit()
}
fn section_repo(repo: Option<&Path>) {
println!("repo:");
match repo {
Some(r) => println!(" path: {}", r.display()),
None => println!(" (not in a git repository)"),
}
}
fn section_global(global: &Global, report: &mut Report) {
println!("global config:");
let Some(source) = &global.source else {
println!(" (none. Using built-in defaults)");
return;
};
println!(" source: {}", source.display());
println!(" theme: {}", global.ui_theme);
println!(" shell prefix: {}", global.shell_prefix);
println!(" git default remote: {}", global.git_default_remote);
if let Some(base) = &global.git_default_base {
println!(" git default base: {base}");
}
if global.projects_roots.is_empty() {
println!(" projects.roots: (none)");
return;
}
println!(" projects.roots: {}", global.projects_roots.len());
for root in &global.projects_roots {
let ok = root.is_dir();
let tag = if ok { "ok" } else { "missing" };
println!(" - {} [{tag}]", root.display());
if !ok {
report.problem(format!(
"projects root does not exist: {}; \
try: remove it from ~/.config/limb/config.toml, or `mkdir -p {}`",
root.display(),
root.display()
));
}
}
}
fn section_repo_config(repo_config: Option<&Repo>, report: &mut Report) {
println!("repo config:");
let Some(r) = repo_config else {
println!(" (no .limb.toml in this repo or any ancestor)");
return;
};
println!(" source: {}", r.source.display());
println!(" root: {}", r.root.display());
println!(" base_dir: {}", r.worktrees_base_dir.display());
section_shared(r, report);
section_templates(r);
check_hooks("repo hooks", &r.hooks, &r.root, report);
}
fn section_shared(r: &Repo, report: &mut Report) {
if r.worktrees_shared.is_empty() {
println!(" shared files: (none)");
return;
}
let source = r.resolved_shared_source();
println!(
" shared files: {} from {}",
r.worktrees_shared.len(),
source.display()
);
if !source.is_dir() {
report.problem(format!(
"shared source dir does not exist: {}; \
try: `mkdir -p {}` and add the files listed under worktrees.shared",
source.display(),
source.display()
));
}
for f in &r.worktrees_shared {
let full = source.join(f);
let tag = if full.exists() { "ok" } else { "missing" };
println!(" - {} [{tag}]", f.display());
if !full.exists() {
report.note(format!(
"shared file missing: {}; `limb setup` will skip it",
full.display()
));
}
}
}
fn section_templates(r: &Repo) {
if r.templates.is_empty() {
println!(" templates: (none)");
return;
}
println!(" templates: {}", r.templates.len());
for name in r.templates.keys() {
println!(" - {name}");
}
}
fn section_discovered(global: &Global, report: &mut Report) {
println!("discovered repos:");
let mut total = 0_usize;
for root in &global.projects_roots {
if !root.is_dir() {
continue;
}
match discover::repos_under(root) {
Ok(repos) => {
println!(" {}: {}", root.display(), repos.len());
total += repos.len();
}
Err(e) => report.problem(format!("cannot scan {}: {e:#}", root.display())),
}
}
println!(" total: {total}");
}
fn check_hooks(label: &str, hooks: &Hooks, root: &Path, report: &mut Report) {
let entries = [
("pre_add", hooks.pre_add.as_deref()),
("post_add", hooks.post_add.as_deref()),
("pre_remove", hooks.pre_remove.as_deref()),
("post_remove", hooks.post_remove.as_deref()),
];
if !entries.iter().any(|(_, p)| p.is_some()) {
println!(" {label}: (none)");
return;
}
println!(" {label}:");
for (name, maybe) in entries {
let Some(path) = maybe else { continue };
let full = root.join(path);
let tag = if full.is_file() { "ok" } else { "missing" };
println!(" - {name}: {} [{tag}]", path.display());
if !full.is_file() {
report.problem(format!(
"hook script missing: {} ({name}); \
try: create the script and `chmod +x {}`",
full.display(),
full.display()
));
}
}
}
#[derive(Default)]
struct Report {
problems: Vec<String>,
notes: Vec<String>,
}
impl Report {
fn problem(&mut self, msg: String) {
self.problems.push(msg);
}
fn note(&mut self, msg: String) {
self.notes.push(msg);
}
fn emit(self) -> Result<()> {
if !self.notes.is_empty() {
let s = style::DIM;
anstream::println!();
anstream::println!("{s}notes:{s:#}");
for n in &self.notes {
anstream::println!(" {s}- {n}{s:#}");
}
}
if self.problems.is_empty() {
let ok = style::OK;
anstream::println!();
anstream::println!("{ok}✓ all checks passed{ok:#}");
Ok(())
} else {
let e = style::ERROR;
anstream::eprintln!();
anstream::eprintln!("{e}problems:{e:#}");
for p in &self.problems {
anstream::eprintln!(" {e}✗{e:#} {p}");
}
anyhow::bail!("{} problem(s) found", self.problems.len())
}
}
}