git_policy_cli/
commit_no_change.rs1use super::capture_invocation::InvocationCapture;
15use super::commit_conclusion::events_with_summary;
16use super::commit_landing_lock::acquire_landing_lock;
17use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
18use super::commit_workspace::CommitWorkspace;
19use super::landing_attempt::{LandingOutcome, LandingPayload, LandingRequest, land_transaction};
20use super::landing_findings::{LANDING_FINDING_EXIT_CODE, landing_finding};
21use super::owner_lock::OwnerLock;
22use super::policy_events::PolicyEvent;
23use super::transaction_git::{GitFailure, GitRequest, run_git_checked};
24
25pub const NO_CHANGE_CORE_ID: &str = "commit-normalization";
27
28pub const NO_CHANGE_CODE: &str = "no-change";
30use super::transaction_journal::{AddedPath, Base, TransactionMode};
31use super::worktree_completion::{InstallResult, install_added_worktree_files};
32
33
34pub struct NoChangeInput<'a> {
44 pub invocation: &'a CommitInvocation<'a>,
46 pub workspace: &'a mut CommitWorkspace,
48 pub capture: &'a InvocationCapture,
50 pub mode: TransactionMode,
52 pub eligible: bool,
54 pub intended_tree: &'a str,
56 pub committed_paths: &'a [String],
58 pub selected_worktree_paths: &'a [AddedPath],
60 pub events: &'a [PolicyEvent],
62 pub changed_passes: u64,
64 pub changed_paths: &'a [String],
66 pub index_lock_timeout_ms: u64,
68}
69
70fn base_tree(input: &NoChangeInput<'_>, base: &str) -> Result<String, GitFailure> {
80 let revision: String = format!("{base}^{{tree}}");
81 let mut request: GitRequest = GitRequest::new(input.invocation.cwd, &["rev-parse", "--verify", revision.as_str()]);
82 request.without_prefix = true;
83 let output = run_git_checked(input.invocation.context, &request)?;
84 return Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
85}
86
87fn no_change_message(conflicted: &[String]) -> String {
97 if conflicted.is_empty() {
98 return String::from(
99 "No commit created: normalization removed every selected change; matching worktree and index copies were reconciled.",
100 );
101 }
102 let listed: String = serde_json::to_string(conflicted).unwrap_or_default();
103 return format!(
104 "No commit created: normalization removed every selected change; index copies were reconciled, but worktree copies changed and were kept: {listed}. Compare them with HEAD before retrying."
105 );
106}
107
108pub fn complete_no_change_transaction(input: NoChangeInput<'_>) -> Result<Option<TransactionResult>, TransactionStop> {
118 let Base::Commit(base) = input.capture.base.clone() else {
119 return Ok(None);
120 };
121 if !input.eligible {
122 return Ok(None);
123 }
124 if base_tree(&input, base.as_str())? != input.intended_tree {
125 return Ok(None);
126 }
127 let summary: Vec<PolicyEvent> = events_with_summary(input.events.to_vec(), input.changed_passes, input.changed_paths);
128 let payload: LandingPayload = LandingPayload::NormalizeOnly {
129 landed_tree_oid: String::from(input.intended_tree),
130 };
131 let request: LandingRequest<'_> = LandingRequest {
132 capture: input.capture,
133 mode: input.mode,
134 payload: &payload,
135 committed_paths: input.committed_paths,
136 added_paths: &[],
137 selected_worktree_paths: input.selected_worktree_paths,
138 index_lock_timeout_ms: input.index_lock_timeout_ms,
139 attempt: 1,
140 };
141 let outcome: LandingOutcome = {
142 let lock: OwnerLock = acquire_landing_lock(
143 input.invocation.context,
144 input.invocation.cwd,
145 &input.capture.layout,
146 input.invocation.environment,
147 )
148 .map_err(|error| { return failed(error.0); })?;
149 land_transaction(
150 input.invocation.context,
151 input.invocation.cwd,
152 &lock,
153 &mut input.workspace.landing,
154 &request,
155 )?
156 };
157 if let Some(event) = landing_finding(&outcome, input.capture, None, false) {
158 let mut events: Vec<PolicyEvent> = summary;
159 events.push(event);
160 return Ok(Some(TransactionResult {
161 events,
162 exit_code: LANDING_FINDING_EXIT_CODE,
163 landed_oid: None,
164 }));
165 }
166 let completion: InstallResult = install_added_worktree_files(
167 input.invocation.context,
168 input.invocation.cwd,
169 input.capture.layout.repository_root.as_path(),
170 input.selected_worktree_paths,
171 Some(input.workspace.object_directory.as_path()),
172 )
173 .map_err(|error| { return TransactionStop::Message(error.0); })?;
174 input.workspace.finish_transaction();
175 let mut events: Vec<PolicyEvent> = summary;
176 events.push(PolicyEvent::CoreFinding {
177 core_id: NO_CHANGE_CORE_ID,
178 code: NO_CHANGE_CODE,
179 message: no_change_message(completion.conflicted.as_slice()),
180 });
181 return Ok(Some(TransactionResult {
182 events,
183 exit_code: 1,
184 landed_oid: None,
185 }));
186}
187
188#[cfg(test)]
190#[path = "commit_no_change_tests.rs"]
191mod tests;