git_policy_cli/
doctor_config.rs1use super::config_file::load_repository_config;
15use super::config_schema::{CliGitConfig, ConcurrencyConfig};
16use super::discarded_prune::Limits;
17use super::config_snapshot::read_config_snapshot;
18use super::external_hooks::{ConfiguredHook, configured_hooks, describe_hook, explain_external_hooks};
19use super::transaction_git::{GitContext, GitRequest, run_git};
20use std::path::{Path, PathBuf};
21
22#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct ConfigFindings {
34 pub hooks: Vec<ConfiguredHook>,
36 pub retired_hooks_key: bool,
38 pub retired_index_lock_key: bool,
40 pub retired_landing_key: bool,
42 pub discarded: Limits,
44}
45
46fn top_level(context: &GitContext, cwd: &Path) -> Option<PathBuf> {
56 let request: GitRequest = GitRequest::new(cwd, &["rev-parse", "--show-toplevel"]);
57 let output = run_git(context, &request).ok()?;
58 if !output.succeeded() {
59 return None;
60 }
61 let text: String = String::from_utf8(output.stdout).ok()?;
62 let trimmed: &str = text.trim_end_matches('\n');
63 if trimmed.is_empty() {
64 return None;
65 }
66 return Some(PathBuf::from(trimmed));
67}
68
69pub fn config_findings(context: &GitContext, cwd: &Path) -> ConfigFindings {
79 let hooks: Vec<ConfiguredHook> = read_config_snapshot(context, cwd)
80 .map(|snapshot| { return configured_hooks(&snapshot); })
81 .unwrap_or_default();
82 let loaded: Option<CliGitConfig> = top_level(context, cwd).and_then(|root| { return load_repository_config(root.as_path()).ok(); }).map(|found| { return found.config; });
85 let concurrency: Option<ConcurrencyConfig> = loaded.as_ref().map(|found| { return found.concurrency; });
86 return ConfigFindings {
87 hooks,
88 retired_hooks_key: concurrency.is_some_and(|retired| { return retired.hooks.present; }),
89 retired_index_lock_key: concurrency.is_some_and(|retired| { return retired.index_lock.present; }),
90 retired_landing_key: concurrency.is_some_and(|retired| { return retired.landing.present; }),
91 discarded: loaded.map_or_else(Limits::default, |found| { return found.discarded; }),
92 };
93}
94
95pub fn render_hook_lines(findings: &ConfigFindings) -> String {
105 let mut text: String = String::new();
106 for hook in &findings.hooks {
107 text.push_str(describe_hook(hook).as_str());
108 text.push('\n');
109 }
110 return text;
111}
112
113pub fn render_notes(findings: &ConfigFindings) -> String {
124 let mut text: String = String::new();
125 if !findings.hooks.is_empty() {
126 text.push_str("git cli-git doctor: ");
127 text.push_str(explain_external_hooks(findings.hooks.as_slice()).as_str());
128 text.push('\n');
129 }
130 if findings.retired_hooks_key {
131 text.push_str(
132 "git cli-git doctor: cli-git.config.jsonc has a `hooks` key (`hooks.concurrentCommits`). It is accepted so \
133 older files still load, and ignored: the wrapper runs no external hook, so there is no hook lock to \
134 tune. Delete the key when convenient.\n",
135 );
136 }
137 if findings.retired_index_lock_key {
138 text.push_str(
139 "git cli-git doctor: cli-git.config.jsonc sets `indexLock.unprovenOwnerTimeoutMs`. The key does nothing: the \
140 commit service waits for another process's `index.lock` for as long as `CLI_GIT_LOCK_WAIT_SECONDS` allows \
141 and reads no timeout from this file. Remove the key from cli-git.config.jsonc.\n",
142 );
143 }
144 if findings.retired_landing_key {
145 text.push_str(
146 "git cli-git doctor: cli-git.config.jsonc sets `landing.reserveAfterLostRaces`. The key does nothing: the \
147 commit service lands commits in the order they arrive and no longer reserves a landing slot for a commit \
148 that lost races. Remove the key from cli-git.config.jsonc.\n",
149 );
150 }
151 return text;
152}
153
154#[cfg(test)]
156#[path = "doctor_config_tests.rs"]
157mod tests;