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::path_bytes::path_message_text;
23use super::policy_events::PolicyEvent;
24use super::transaction_git::{GitFailure, GitRequest, run_git_checked};
25
26pub const NO_CHANGE_CORE_ID: &str = "commit-normalization";
28
29pub const NO_CHANGE_CODE: &str = "no-change";
31use super::transaction_journal::{AddedPath, Base, TransactionMode};
32use super::worktree_completion::{InstallResult, install_added_worktree_files};
33
34
35pub struct NoChangeInput<'a> {
45 pub invocation: &'a CommitInvocation<'a>,
47 pub workspace: &'a mut CommitWorkspace,
49 pub capture: &'a InvocationCapture,
51 pub mode: TransactionMode,
53 pub eligible: bool,
55 pub intended_tree: &'a str,
57 pub committed_paths: &'a [Vec<u8>],
59 pub selected_worktree_paths: &'a [AddedPath],
61 pub events: &'a [PolicyEvent],
63 pub changed_passes: u64,
65 pub changed_paths: &'a [Vec<u8>],
67 pub index_lock_timeout_ms: u64,
69}
70
71fn base_tree(input: &NoChangeInput<'_>, base: &str) -> Result<String, GitFailure> {
81 let revision: String = format!("{base}^{{tree}}");
82 let mut request: GitRequest = GitRequest::new(input.invocation.cwd, &["rev-parse", "--verify", revision.as_str()]);
83 request.without_prefix = true;
84 let output = run_git_checked(input.invocation.context, &request)?;
85 return Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
86}
87
88fn no_change_message(conflicted: &[Vec<u8>]) -> String {
98 if conflicted.is_empty() {
99 return String::from(
100 "No commit created: normalization removed every selected change; matching worktree and index copies were reconciled.",
101 );
102 }
103 let names: Vec<String> = conflicted.iter().map(|path| { return path_message_text(path.as_slice()); }).collect();
105 let listed: String = serde_json::to_string(&names).unwrap_or_default();
106 return format!(
107 "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."
108 );
109}
110
111pub fn complete_no_change_transaction(input: NoChangeInput<'_>) -> Result<Option<TransactionResult>, TransactionStop> {
121 let Base::Commit(base) = input.capture.base.clone() else {
122 return Ok(None);
123 };
124 if !input.eligible {
125 return Ok(None);
126 }
127 if base_tree(&input, base.as_str())? != input.intended_tree {
128 return Ok(None);
129 }
130 let summary: Vec<PolicyEvent> = events_with_summary(input.events.to_vec(), input.changed_passes, input.changed_paths);
131 let payload: LandingPayload = LandingPayload::NormalizeOnly {
132 landed_tree_oid: String::from(input.intended_tree),
133 };
134 let request: LandingRequest<'_> = LandingRequest {
135 capture: input.capture,
136 mode: input.mode,
137 payload: &payload,
138 committed_paths: input.committed_paths,
139 added_paths: &[],
140 selected_worktree_paths: input.selected_worktree_paths,
141 index_lock_timeout_ms: input.index_lock_timeout_ms,
142 attempt: 1,
143 };
144 let outcome: LandingOutcome = {
145 let lock: OwnerLock = acquire_landing_lock(
146 input.invocation.context,
147 input.invocation.cwd,
148 &input.capture.layout,
149 input.invocation.environment,
150 )
151 .map_err(|error| { return failed(error.0); })?;
152 land_transaction(
153 input.invocation.context,
154 input.invocation.cwd,
155 &lock,
156 &mut input.workspace.landing,
157 &request,
158 )?
159 };
160 if let Some(event) = landing_finding(&outcome, input.capture, None, false) {
161 let mut events: Vec<PolicyEvent> = summary;
162 events.push(event);
163 return Ok(Some(TransactionResult {
164 events,
165 exit_code: LANDING_FINDING_EXIT_CODE,
166 landed_oid: None,
167 }));
168 }
169 let completion: InstallResult = install_added_worktree_files(
170 input.invocation.context,
171 input.invocation.cwd,
172 input.capture.layout.repository_root.as_path(),
173 input.selected_worktree_paths,
174 Some(input.workspace.object_directory.as_path()),
175 )
176 .map_err(|error| { return TransactionStop::Message(error.0); })?;
177 input.workspace.finish_transaction();
178 let mut events: Vec<PolicyEvent> = summary;
179 events.push(PolicyEvent::CoreFinding {
180 core_id: NO_CHANGE_CORE_ID,
181 code: NO_CHANGE_CODE,
182 message: no_change_message(completion.conflicted.as_slice()),
183 });
184 return Ok(Some(TransactionResult {
185 events,
186 exit_code: 1,
187 landed_oid: None,
188 }));
189}
190
191#[cfg(test)]
193#[path = "commit_no_change_tests.rs"]
194mod tests;