use crate::config_file::load_repository_config;
use crate::config_schema::{CliGitConfig, ConcurrencyConfig};
use crate::discarded_prune::Limits;
use crate::config_snapshot::read_config_snapshot;
use crate::external_hooks::{ConfiguredHook, configured_hooks, describe_hook, explain_external_hooks};
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigFindings {
pub hooks: Vec<ConfiguredHook>,
pub retired_hooks_key: bool,
pub retired_index_lock_key: bool,
pub retired_landing_key: bool,
pub discarded: Limits,
}
fn top_level(context: &GitContext, cwd: &Path) -> Option<PathBuf> {
let request: GitRequest = GitRequest::new(cwd, &["rev-parse", "--show-toplevel"]);
let output = run_git(context, &request).ok()?;
if !output.succeeded() {
return None;
}
let text: String = String::from_utf8(output.stdout).ok()?;
let trimmed: &str = text.trim_end_matches('\n');
if trimmed.is_empty() {
return None;
}
return Some(PathBuf::from(trimmed));
}
pub fn config_findings(context: &GitContext, cwd: &Path) -> ConfigFindings {
let hooks: Vec<ConfiguredHook> = read_config_snapshot(context, cwd)
.map(|snapshot| { return configured_hooks(&snapshot); })
.unwrap_or_default();
let loaded: Option<CliGitConfig> = top_level(context, cwd).and_then(|root| { return load_repository_config(root.as_path()).ok(); }).map(|found| { return found.config; });
let concurrency: Option<ConcurrencyConfig> = loaded.as_ref().map(|found| { return found.concurrency; });
return ConfigFindings {
hooks,
retired_hooks_key: concurrency.is_some_and(|retired| { return retired.hooks.present; }),
retired_index_lock_key: concurrency.is_some_and(|retired| { return retired.index_lock.present; }),
retired_landing_key: concurrency.is_some_and(|retired| { return retired.landing.present; }),
discarded: loaded.map_or_else(Limits::default, |found| { return found.discarded; }),
};
}
pub fn render_hook_lines(findings: &ConfigFindings) -> String {
let mut text: String = String::new();
for hook in &findings.hooks {
text.push_str(describe_hook(hook).as_str());
text.push('\n');
}
return text;
}
pub fn render_notes(findings: &ConfigFindings) -> String {
let mut text: String = String::new();
if !findings.hooks.is_empty() {
text.push_str("git cli-git doctor: ");
text.push_str(explain_external_hooks(findings.hooks.as_slice()).as_str());
text.push('\n');
}
if findings.retired_hooks_key {
text.push_str(
"git cli-git doctor: cli-git.config.jsonc has a `hooks` key (`hooks.concurrentCommits`). It is accepted so \
older files still load, and ignored: the wrapper runs no external hook, so there is no hook lock to \
tune. Delete the key when convenient.\n",
);
}
if findings.retired_index_lock_key {
text.push_str(
"git cli-git doctor: cli-git.config.jsonc sets `indexLock.unprovenOwnerTimeoutMs`. The key does nothing: the \
commit service waits for another process's `index.lock` for as long as the lock stays there, whether the process \
is proven to hold it or not, says what it knows about the owner while it waits, and reads no timeout from this \
file. Remove the key from cli-git.config.jsonc.\n",
);
}
if findings.retired_landing_key {
text.push_str(
"git cli-git doctor: cli-git.config.jsonc sets `landing.reserveAfterLostRaces`. The key does nothing: the \
commit service lands commits in the order they arrive and no longer reserves a landing slot for a commit \
that lost races. Remove the key from cli-git.config.jsonc.\n",
);
}
return text;
}
#[cfg(test)]
#[path = "doctor_config_tests.rs"]
mod tests;