1use crate::json_record::quote;
16use crate::manual_push_cause::ManualPushCause;
17
18pub const SELF_HINTED: &[(&str, &str)] = &[
20 ("require-root", "not-at-root"),
21 ("linked-worktree-only", "linked-worktree-required"),
22 ("branch-worktree-only", "branch-creation-requires-worktree"),
23];
24
25pub fn finding_hint(policy: &str, code: &str, fix_available: bool) -> Option<&'static str> {
36 match (policy, code, fix_available) {
37 ("add-explicit", "bulk-add-rejected", _) => {
38 return Some(
39 "Hint: if you meant to stage everything you changed, run `git status --short` to list it and name \
40 each path, as in `git add -- <path> <path>`; if you meant only files git already tracks, name those \
41 paths the same way.",
42 );
43 }
44 ("final-newline", "noncanonical-final-newline", true) => {
45 return Some(
46 "Hint: if you are committing or checking this file, a commit corrects the ending for you; \
47 to correct it now without committing, run `git cli-git fix -- <path>`.",
48 );
49 }
50 ("final-newline", "noncanonical-final-newline", false) => {
51 return Some(
52 "Hint: if you are committing or checking this file, edit it so it ends with exactly one newline, \
53 then stage it again.",
54 );
55 }
56 ("markdown/autofix", "markdown-autofix", _) => {
57 return Some(
58 "Hint: if you are committing this Markdown, the commit applies the linter's corrections itself; \
59 to apply them now, run `git cli-git fix -- <path>`.",
60 );
61 }
62 ("markdown/autofix", "markdown-violation", _) => {
63 return Some(
64 "Hint: if you are committing this Markdown, the linter cannot repair this rule by itself; \
65 edit the file at the reported line and column, stage it again, and retry.",
66 );
67 }
68 ("mono/forbidden-root-context", "root-context-forbidden", _) => {
69 return Some(
70 "Hint: if you meant to keep project notes or terms for later readers, put them in a file under doc/; \
71 to drop CONTEXT.md from this commit and keep the file on disk, run `git rm --cached CONTEXT.md`.",
72 );
73 }
74 ("mono/dependent-version-bump", "dependent-version-stale", _) => {
75 return Some(
76 "Hint: if you changed a package that others depend on, raise its dependents' version ranges to match \
77 the new version in the same commit, then stage those files; `git cli-git fix -- <path>` applies the \
78 correction when one is available.",
79 );
80 }
81 ("mono/dependent-version-bump", "dependent-version-unsupported", _) => {
82 return Some(
83 "Hint: if you edited a dependency declaration of a shape this check does not read, update the \
84 dependents by hand in the same commit; the message names the file.",
85 );
86 }
87 ("mono/catalog-specifiers", "disallowed-specifier", _) => {
88 return Some(
89 "Hint: if you are adding or changing a dependency, move its version into the catalog section of \
90 pnpm-workspace.yaml and write catalog: here, or write workspace:* for a package of this workspace; \
91 peerDependencies take * or catalog:.",
92 );
93 }
94 ("mono/catalog-specifiers", "non-string-specifier", _) => {
95 return Some(
96 "Hint: if you are writing a dependency version, make it a string such as catalog: or workspace:*; \
97 a number, list, object or null is not a specifier.",
98 );
99 }
100 ("mono/catalog-specifiers", "malformed-section", _) => {
101 return Some(
102 "Hint: if you are editing a dependency section, write it as an object that maps package names to \
103 specifiers; the message names the section.",
104 );
105 }
106 ("mono/catalog-specifiers", "duplicate-key", _) => {
107 return Some(
108 "Hint: if you are editing this manifest by hand, keep one copy of the repeated section or \
109 dependency name; pnpm keeps the last copy, which may not be the one you meant.",
110 );
111 }
112 ("mono/catalog-specifiers", "invalid-manifest", _) => {
113 return Some(
114 "Hint: if you are committing this package.json, make it a JSON object that parses and is UTF-8 \
115 text (a stray character, an empty file or a link instead of a file shows here), then stage it again.",
116 );
117 }
118 ("mono/catalog-specifiers", "unsupported-manifest-format", _) => {
119 return Some(
120 "Hint: if you are adding a package.json5, package.yaml or package.yml, use package.json instead, \
121 because the catalog rule reads only that format; rename or convert the file and stage it again.",
122 );
123 }
124 ("security/forbidden-strings", "forbidden-string", _) => {
125 return Some(
126 "Hint: if you are committing text or a path that contains a secret or a banned term, remove or \
127 replace it where the message points and stage the file again; if you believe the rule is wrong, \
128 review it in the rules file named by the configuration instead of splitting the commit.",
129 );
130 }
131 _ => return None,
132 }
133}
134
135pub fn manual_push_failure_hint(cause: ManualPushCause) -> Option<&'static str> {
147 return match cause {
148 ManualPushCause::ContentMissing => engine_failure_hint("content-unavailable"),
149 ManualPushCause::RemoteMoved => Some(
150 "Hint: the remote changed while the wrapper was reading it, so nothing was pushed; run the same push again.",
151 ),
152 ManualPushCause::RemoteUnreachable => Some(
153 "Hint: the wrapper could not read the remote's branches before pushing, so nothing was pushed; check the \
154 network connection, the remote's address and your credentials (`git ls-remote <remote>` shows Git's own \
155 answer), then run the push again.",
156 ),
157 ManualPushCause::GitNotStarted => Some(
158 "Hint: the wrapper could not start Git to check this push, so nothing was pushed; check that the Git \
159 installation the wrapper runs exists and can be executed, then run the push again.",
160 ),
161 ManualPushCause::GitSilent => Some(
162 "Hint: Git ended the wrapper's check of this push without a message, so nothing was pushed; run the same \
163 command with `--dry-run` to see Git's own answer, and run the push again.",
164 ),
165 ManualPushCause::UnexpectedOutput => Some(
166 "Hint: Git answered in a form the wrapper does not read, so nothing was pushed; this is a defect of the \
167 wrapper, not of the repository. Report it with this message and the output of `git --version`.",
168 ),
169 ManualPushCause::UnsupportedObject => Some(
170 "Hint: the manual-push policies read commits only; push a branch, or a tag that points at a commit.",
171 ),
172 ManualPushCause::SelfExplained => None,
173 };
174}
175
176pub fn engine_failure_hint(code: &str) -> Option<&'static str> {
187 match code {
188 "config-invalid" => {
189 return Some(
190 "Hint: if you just edited cli-git.config.jsonc, fix the key the message names or revert the edit; \
191 an unknown key, a wrong type or an unknown policy id fails before any policy runs.",
192 );
193 }
194 "core-incomplete" => {
195 return Some(
196 "Hint: if another session was changing the repository at the same moment (a commit, a checkout or a \
197 rebase running elsewhere), run the command again; if it repeats, run `git status` to check that the \
198 repository is healthy.",
199 );
200 }
201 "content-unavailable" => {
202 return Some(
203 "Hint: if you are committing or pushing content that is not in this clone (a missing object, a \
204 shallow history, or a file removed during the command), fetch the history with `git fetch`, make \
205 sure the files exist, and retry.",
206 );
207 }
208 "policy-incomplete" => {
209 return Some(
210 "Hint: if you are committing or pushing, a policy could not finish reading the content, so it \
211 decided nothing; fix the cause the message names (an unreadable file, a path with a line break) \
212 and retry.",
213 );
214 }
215 "patch-invalid" | "patch-conflict" => {
216 return Some(
217 "Hint: if you are committing files that changed while the command ran or that another policy also \
218 rewrites, save your edits, run `git cli-git fix -- <path>` to apply the corrections, review the \
219 result with `git diff`, and commit again.",
220 );
221 }
222 "fix-cycle" | "fix-pass-limit" => {
223 return Some(
224 "Hint: two corrections keep undoing each other on this file; apply one of them by hand, stage the \
225 file, and commit again. Report the pair of policies if the file is ordinary text.",
226 );
227 }
228 "transaction-failed" => {
229 return Some(
230 "Hint: if you were committing, run `git status` and `git log --oneline -3` to see whether the \
231 commit landed before you retry; staged files stay staged unless the message says otherwise, and the \
232 message names the step that failed.",
233 );
234 }
235 "legacy-config-ignored" => {
236 return Some(
237 "Hint: if you are still keeping the old configuration files, delete them once \
238 cli-git.config.jsonc holds the settings; they are no longer read.",
239 );
240 }
241 "traditional-hooks-unsupported" => {
242 return Some(
243 "Hint: scripts in the hooks directory never run through cli-git. If one still checks something for you, \
244 express it as a policy under policies in cli-git.config.jsonc; `git cli-git doctor` lists the \
245 scripts it finds and exits 1 while any remain.",
246 );
247 }
248 _ => return None,
249 }
250}
251
252pub const WARN_UNSAFE_HINT: &str = "Hint: if you set it to warn to keep commits flowing, note that this policy \
263 changes or misses content when it only warns; set its severity to error to enforce it, or to off to disable it, \
264 in cli-git.config.jsonc.";
265
266pub const POST_COMMIT_BLOCKED_HINT: &str = "Hint: if you wanted this commit backed up, fix what the gate reported \
268 and run `git push` yourself; the commit stays in your local history until then.";
269
270pub fn push_hint_field(line: &mut String, hint: Option<&str>) {
282 if let Some(text) = hint {
283 line.push_str(",\"hint\":");
284 line.push_str(quote(text).as_str());
285 }
286}
287
288#[cfg(test)]
290#[path = "event_hints_tests.rs"]
291mod tests;