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