1use super::candidate_object::CandidateMode;
16use super::diagnostic_log::debug;
17use super::direct_fix_install::InstallChange;
18use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
19use super::transaction_journal::AddedPath;
20use super::worktree_completion::{WorktreeFileState, inspect_worktree_file};
21use std::ffi::OsString;
22use std::path::Path;
23
24const TAG: &str = "commitWorktreeRecords";
26
27const RAW_FIELD_COUNT: usize = 5;
29
30#[derive(Clone, Debug, Default, Eq, PartialEq)]
40pub struct HookChanges {
41 pub paths: Vec<String>,
43 pub worktree_records: Vec<AddedPath>,
45}
46
47impl HookChanges {
48 pub fn combine(earlier: &HookChanges, later: &HookChanges) -> HookChanges {
58 let mut paths: Vec<String> = earlier.paths.clone();
59 for path in &later.paths {
60 if !paths.contains(path) {
61 paths.push(path.clone());
62 }
63 }
64 let mut worktree_records: Vec<AddedPath> = earlier.worktree_records.clone();
65 worktree_records.extend(later.worktree_records.iter().cloned());
66 return HookChanges {
67 paths,
68 worktree_records,
69 };
70 }
71}
72
73fn parse_hook_changes(output: &str) -> Result<HookChanges, GitFailure> {
83 let fields: Vec<&str> = output.split('\0').collect();
84 let mut changes: HookChanges = HookChanges::default();
85 let mut index: usize = 0;
86 while index < fields.len() {
87 let field: &str = fields[index];
88 if field.is_empty() {
89 index += 1;
90 continue;
91 }
92 let Some(path) = fields.get(index + 1) else {
93 return Err(GitFailure(format!("git diff-tree returned a malformed record: {field:?}")));
94 };
95 let metadata: Vec<&str> = field.trim_start_matches(':').split(' ').collect();
96 if metadata.len() != RAW_FIELD_COUNT {
97 return Err(GitFailure(format!("git diff-tree returned a malformed record: {field:?}")));
98 }
99 let (from_mode, to_mode, from_oid, to_oid) = (metadata[0], metadata[1], metadata[2], metadata[3]);
100 changes.paths.push(String::from(*path));
101 if from_mode == to_mode && (to_mode == "100644" || to_mode == "100755") {
102 changes.worktree_records.push(AddedPath {
103 path: String::from(*path),
104 git_mode: String::from(to_mode),
105 original_oid: String::from(from_oid),
106 intended_oid: String::from(to_oid),
107 });
108 }
109 index += 2;
110 }
111 return Ok(changes);
112}
113
114pub fn describe_hook_changes(
124 context: &GitContext,
125 cwd: &Path,
126 object_directory: &Path,
127 from_tree: &str,
128 to_tree: &str,
129) -> Result<HookChanges, GitFailure> {
130 if from_tree == to_tree {
131 return Ok(HookChanges::default());
132 }
133 let mut request: GitRequest = GitRequest::new(
134 cwd,
135 &["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-abbrev", from_tree, to_tree],
136 );
137 request.without_prefix = true;
138 request.object_directory = Some(object_directory.to_path_buf());
139 let output: GitOutput = run_git_checked(context, &request)?;
140 let text: &str = std::str::from_utf8(output.stdout.as_slice())
141 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
142 let changes: HookChanges = parse_hook_changes(text)?;
143 debug(
144 TAG,
145 format!("pre-commit changed {} staged paths; the commit keeps them as native Git does", changes.paths.len()).as_str(),
146 );
147 return Ok(changes);
148}
149
150fn ordinary_git_mode(mode: CandidateMode) -> Option<&'static str> {
160 return match mode {
161 CandidateMode::Regular => Some("100644"),
162 CandidateMode::Executable => Some("100755"),
163 _ => None,
164 };
165}
166
167fn blob_id(context: &GitContext, cwd: &Path, bytes: &[u8]) -> Result<String, GitFailure> {
177 let mut request: GitRequest = GitRequest::new(cwd, &["hash-object", "--stdin"]);
178 request.without_prefix = true;
179 request.input = Some(bytes.to_vec());
180 let output: GitOutput = run_git_checked(context, &request)?;
181 return Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
182}
183
184pub fn settled_entry(
194 context: &GitContext,
195 cwd: &Path,
196 index: &Path,
197 path: &[u8],
198) -> Result<Option<(String, String)>, GitFailure> {
199 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
200 pathspec.extend_from_slice(path);
201 let mut request: GitRequest = GitRequest::new(cwd, &["ls-files", "--stage", "-z", "--"]);
202 request.arguments.push(os_from_bytes(pathspec));
203 request.without_prefix = true;
204 request.index = Some(index.to_path_buf());
205 let output: GitOutput = run_git_checked(context, &request)?;
206 for record in output.stdout.split(|byte| { return *byte == 0; }) {
207 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
208 continue;
209 };
210 let metadata: String = String::from_utf8_lossy(&record[..tab]).into_owned();
211 let fields: Vec<&str> = metadata.split(' ').collect();
212 if fields.len() == 3 && fields[2] == "0" {
213 return Ok(Some((String::from(fields[0]), String::from(fields[1]))));
214 }
215 }
216 return Ok(None);
217}
218
219fn os_from_bytes(bytes: Vec<u8>) -> OsString {
229 #[cfg(unix)]
230 {
231 use std::os::unix::ffi::OsStringExt;
232 return OsString::from_vec(bytes);
233 }
234 #[cfg(not(unix))]
235 {
236 return OsString::from(String::from_utf8_lossy(bytes.as_slice()).into_owned());
237 }
238}
239
240pub fn selected_worktree_records(
251 context: &GitContext,
252 cwd: &Path,
253 repository_root: &Path,
254 index: &Path,
255 changes: &[InstallChange],
256) -> Result<Vec<AddedPath>, GitFailure> {
257 let mut records: Vec<AddedPath> = Vec::new();
258 for change in changes {
259 let Some(git_mode) = ordinary_git_mode(change.mode) else {
260 continue;
261 };
262 let original_oid: String = blob_id(context, cwd, &change.original)?;
263 let Some((entry_mode, entry_oid)) = settled_entry(context, cwd, index, change.path.as_slice())? else {
264 continue;
265 };
266 if entry_mode != git_mode || entry_oid == original_oid {
267 continue;
268 }
269 let destination = repository_root.join(path_from_bytes(change.path.as_slice()));
270 match inspect_worktree_file(destination.as_path(), git_mode, &change.original, &change.replacement) {
271 Ok(WorktreeFileState::Original(_)) => {}
272 Ok(_) => continue,
274 Err(error) => {
275 return Err(GitFailure(format!(
276 "reading {} failed: {error}",
277 destination.display()
278 )));
279 }
280 }
281 records.push(AddedPath {
282 path: String::from_utf8_lossy(change.path.as_slice()).into_owned(),
283 git_mode: String::from(git_mode),
284 original_oid,
285 intended_oid: entry_oid,
286 });
287 }
288 return Ok(records);
289}
290
291fn path_from_bytes(bytes: &[u8]) -> std::path::PathBuf {
301 return std::path::PathBuf::from(os_from_bytes(bytes.to_vec()));
302}
303
304#[cfg(test)]
306#[path = "commit_worktree_records_tests.rs"]
307mod tests;