1use super::candidate_object::CandidateMode;
16use super::diagnostic_log::debug;
17use super::direct_fix_install::InstallChange;
18use super::path_bytes::os_from_path_bytes;
19use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
20use super::transaction_journal::AddedPath;
21use super::worktree_completion::{WorktreeFileState, inspect_worktree_file};
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<Vec<u8>>,
43 pub worktree_records: Vec<AddedPath>,
45}
46
47impl HookChanges {
48 pub fn combine(earlier: &HookChanges, later: &HookChanges) -> HookChanges {
58 let mut paths: Vec<Vec<u8>> = 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: &[u8]) -> Result<HookChanges, GitFailure> {
83 let fields: Vec<&[u8]> = output.split(|byte| { return *byte == 0; }).collect();
84 let mut changes: HookChanges = HookChanges::default();
85 let mut index: usize = 0;
86 while index < fields.len() {
87 let field: String = String::from_utf8_lossy(fields[index]).into_owned();
89 if field.is_empty() {
90 index += 1;
91 continue;
92 }
93 let Some(path) = fields.get(index + 1) else {
94 return Err(GitFailure(format!("git diff-tree returned a malformed record: {field:?}")));
95 };
96 let metadata: Vec<&str> = field.trim_start_matches(':').split(' ').collect();
97 if metadata.len() != RAW_FIELD_COUNT {
98 return Err(GitFailure(format!("git diff-tree returned a malformed record: {field:?}")));
99 }
100 let (from_mode, to_mode, from_oid, to_oid) = (metadata[0], metadata[1], metadata[2], metadata[3]);
101 changes.paths.push(path.to_vec());
102 if from_mode == to_mode && (to_mode == "100644" || to_mode == "100755") {
103 changes.worktree_records.push(AddedPath {
104 path: path.to_vec(),
105 git_mode: String::from(to_mode),
106 original_oid: String::from(from_oid),
107 intended_oid: String::from(to_oid),
108 });
109 }
110 index += 2;
111 }
112 return Ok(changes);
113}
114
115pub fn describe_hook_changes(
125 context: &GitContext,
126 cwd: &Path,
127 object_directory: &Path,
128 from_tree: &str,
129 to_tree: &str,
130) -> Result<HookChanges, GitFailure> {
131 if from_tree == to_tree {
132 return Ok(HookChanges::default());
133 }
134 let mut request: GitRequest = GitRequest::new(
135 cwd,
136 &["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-abbrev", from_tree, to_tree],
137 );
138 request.without_prefix = true;
139 request.object_directory = Some(object_directory.to_path_buf());
140 let output: GitOutput = run_git_checked(context, &request)?;
141 let changes: HookChanges = parse_hook_changes(output.stdout.as_slice())?;
142 debug(
143 TAG,
144 format!("pre-commit changed {} staged paths; the commit keeps them as native Git does", changes.paths.len()).as_str(),
145 );
146 return Ok(changes);
147}
148
149fn ordinary_git_mode(mode: CandidateMode) -> Option<&'static str> {
159 return match mode {
160 CandidateMode::Regular => Some("100644"),
161 CandidateMode::Executable => Some("100755"),
162 _ => None,
163 };
164}
165
166fn blob_id(context: &GitContext, cwd: &Path, bytes: &[u8]) -> Result<String, GitFailure> {
176 let mut request: GitRequest = GitRequest::new(cwd, &["hash-object", "--stdin"]);
177 request.without_prefix = true;
178 request.input = Some(bytes.to_vec());
179 let output: GitOutput = run_git_checked(context, &request)?;
180 return Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
181}
182
183pub fn settled_entry(
193 context: &GitContext,
194 cwd: &Path,
195 index: &Path,
196 path: &[u8],
197) -> Result<Option<(String, String)>, GitFailure> {
198 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
199 pathspec.extend_from_slice(path);
200 let mut request: GitRequest = GitRequest::new(cwd, &["ls-files", "--stage", "-z", "--"]);
201 request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
202 request.without_prefix = true;
203 request.index = Some(index.to_path_buf());
204 let output: GitOutput = run_git_checked(context, &request)?;
205 for record in output.stdout.split(|byte| { return *byte == 0; }) {
206 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
207 continue;
208 };
209 let metadata: String = String::from_utf8_lossy(&record[..tab]).into_owned();
210 let fields: Vec<&str> = metadata.split(' ').collect();
211 if fields.len() == 3 && fields[2] == "0" {
212 return Ok(Some((String::from(fields[0]), String::from(fields[1]))));
213 }
214 }
215 return Ok(None);
216}
217
218pub fn selected_worktree_records(
229 context: &GitContext,
230 cwd: &Path,
231 repository_root: &Path,
232 index: &Path,
233 changes: &[InstallChange],
234) -> Result<Vec<AddedPath>, GitFailure> {
235 let mut records: Vec<AddedPath> = Vec::new();
236 for change in changes {
237 let Some(git_mode) = ordinary_git_mode(change.mode) else {
238 continue;
239 };
240 let original_oid: String = blob_id(context, cwd, &change.original)?;
241 let Some((entry_mode, entry_oid)) = settled_entry(context, cwd, index, change.path.as_slice())? else {
242 continue;
243 };
244 if entry_mode != git_mode || entry_oid == original_oid {
245 continue;
246 }
247 let destination = repository_root.join(path_from_bytes(change.path.as_slice()));
248 match inspect_worktree_file(destination.as_path(), git_mode, &change.original, &change.replacement) {
249 Ok(WorktreeFileState::Original(_)) => {}
250 Ok(_) => continue,
252 Err(error) => {
253 return Err(GitFailure(format!(
254 "reading {} failed: {error}",
255 destination.display()
256 )));
257 }
258 }
259 records.push(AddedPath {
260 path: change.path.clone(),
261 git_mode: String::from(git_mode),
262 original_oid,
263 intended_oid: entry_oid,
264 });
265 }
266 return Ok(records);
267}
268
269fn path_from_bytes(bytes: &[u8]) -> std::path::PathBuf {
279 return std::path::PathBuf::from(os_from_path_bytes(bytes));
280}
281
282#[cfg(test)]
284#[path = "commit_worktree_records_tests.rs"]
285mod tests;