1use super::commit_index::copy_index_with_timestamps;
18use super::recovery_files::files_equal;
20use super::diagnostic_log::warn;
22use super::path_bytes::{os_from_path_bytes, path_message_text};
24use super::transaction_journal::TransactionMode;
26use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git, run_git_checked};
28use std::collections::HashMap;
30use std::ffi::OsString;
32use std::path::Path;
33
34const TAG: &str = "computeLandingPostIndex";
36
37pub struct PostIndexInput<'input> {
47 pub mode: TransactionMode,
49 pub pre_landing_index: &'input Path,
51 pub captured_index: &'input Path,
53 pub landed_index: &'input Path,
55 pub post_index: &'input Path,
57 pub landed_tree: &'input str,
59 pub base_revision: &'input str,
61 pub committed_paths: &'input [Vec<u8>],
63 pub guarded_paths: &'input [Vec<u8>],
65 pub exact_private_index: bool,
67}
68
69fn literal_pathspecs() -> Vec<(OsString, OsString)> {
79 return vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
80}
81
82fn literal_pathspec(path: &[u8]) -> OsString {
92 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
93 pathspec.extend_from_slice(path);
94 return os_from_path_bytes(pathspec.as_slice());
95}
96
97fn records(bytes: &[u8]) -> Vec<Vec<u8>> {
107 return bytes
108 .split(|byte| { return *byte == 0; })
109 .filter(|record| { return !record.is_empty(); })
110 .map(<[u8]>::to_vec)
111 .collect();
112}
113
114fn list_stages(
124 context: &GitContext,
125 cwd: &Path,
126 index: &Path,
127 paths: &[Vec<u8>],
128) -> Result<HashMap<Vec<u8>, Vec<Vec<u8>>>, GitFailure> {
129 let mut grouped: HashMap<Vec<u8>, Vec<Vec<u8>>> = HashMap::new();
130 if paths.is_empty() {
131 return Ok(grouped);
132 }
133 let mut arguments: Vec<OsString> = ["ls-files", "--stage", "-z", "--full-name", "--"]
134 .iter()
135 .map(OsString::from)
136 .collect();
137 arguments.extend(paths.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
138 let mut request = GitRequest::new(cwd, &arguments);
139 request.index = Some(index.to_path_buf());
140 request.environment = literal_pathspecs();
141 let output: GitOutput = run_git_checked(context, &request)?;
142 for record in records(&output.stdout) {
143 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
144 return Err(GitFailure(format!("ls-files record has no path: {}", path_message_text(record.as_slice()))));
145 };
146 grouped.entry(record[tab + 1..].to_vec()).or_default().push(record.clone());
147 }
148 return Ok(grouped);
149}
150
151fn changed_paths(context: &GitContext, cwd: &Path, from: &str, to: &str) -> Result<Vec<Vec<u8>>, GitFailure> {
161 let request = GitRequest::new(cwd, &["diff-tree", "-r", "--no-renames", "--name-only", "-z", from, to]);
162 let output: GitOutput = run_git_checked(context, &request)?;
163 return Ok(records(&output.stdout));
164}
165
166fn changed_since_captured(
176 context: &GitContext,
177 cwd: &Path,
178 captured: &Path,
179 landed_tree: &str,
180) -> Result<Vec<Vec<u8>>, GitFailure> {
181 let mut request = GitRequest::new(cwd, &["write-tree"]);
182 request.index = Some(captured.to_path_buf());
183 let output: GitOutput = run_git(context, &request)
184 .map_err(|error| { return GitFailure(format!("git write-tree could not start: {error}")); })?;
185 if !output.succeeded() {
186 return Ok(Vec::new());
187 }
188 let tree: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
189 return changed_paths(context, cwd, tree.as_str(), landed_tree);
190}
191
192fn merge_landed_entries(
203 context: &GitContext,
204 cwd: &Path,
205 artifacts: &Artifacts,
206 changed: &[Vec<u8>],
207) -> Result<(), GitFailure> {
208 let captured = list_stages(context, cwd, artifacts.captured, changed)?;
209 let current = list_stages(context, cwd, artifacts.post, changed)?;
210 let landed = list_stages(context, cwd, artifacts.landed, changed)?;
211 let untouched: Vec<&Vec<u8>> = changed
212 .iter()
213 .filter(|path| {
214 return captured.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[])
215 == current.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
216 })
217 .collect();
218 if untouched.is_empty() {
219 return Ok(());
220 }
221 let zero: String = "0".repeat(artifacts.landed_tree.len());
222 let mut body: Vec<u8> = Vec::new();
223 for path in untouched {
224 match landed.get(path.as_slice()) {
225 Some(entries) => {
226 for entry in entries {
227 body.extend_from_slice(entry.as_slice());
228 body.push(0);
229 }
230 }
231 None => {
232 body.extend_from_slice(format!("0 {zero}\t").as_bytes());
233 body.extend_from_slice(path.as_slice());
234 body.push(0);
235 }
236 }
237 }
238 let mut request = GitRequest::new(cwd, &["update-index", "-z", "--index-info"]);
239 request.index = Some(artifacts.post.to_path_buf());
240 request.input = Some(body);
241 run_git_checked(context, &request)?;
242 return Ok(());
243}
244
245struct Artifacts<'artifacts> {
255 captured: &'artifacts Path,
257 post: &'artifacts Path,
259 landed: &'artifacts Path,
261 landed_tree: &'artifacts str,
263}
264
265fn reset_guarded_paths(
275 context: &GitContext,
276 cwd: &Path,
277 artifacts: &Artifacts,
278 guarded: &[Vec<u8>],
279) -> Result<(), GitFailure> {
280 if guarded.is_empty() {
281 return Ok(());
282 }
283 let captured = list_stages(context, cwd, artifacts.captured, guarded)?;
284 let current = list_stages(context, cwd, artifacts.post, guarded)?;
285 let mut untouched: Vec<&Vec<u8>> = Vec::new();
286 for path in guarded {
287 let before: &[Vec<u8>] = captured.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
288 let now: &[Vec<u8>] = current.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
289 if before == now {
290 untouched.push(path);
291 } else {
292 let shown: String = path_message_text(path.as_slice());
293 warn(
294 TAG,
295 &format!(
296 "A commit hook changed {shown}, but it was staged again while the commit ran; the committed bytes are in HEAD and your staged entry was kept. Compare them with git diff --cached -- {shown}."
297 ),
298 );
299 }
300 }
301 if untouched.is_empty() {
302 return Ok(());
303 }
304 let mut arguments: Vec<OsString> = vec!["reset".into(), "--quiet".into(), artifacts.landed_tree.into(), "--".into()];
305 arguments.extend(untouched.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
306 let mut request = GitRequest::new(cwd, &arguments);
307 request.index = Some(artifacts.post.to_path_buf());
308 request.environment = literal_pathspecs();
309 run_git_checked(context, &request)?;
310 return Ok(());
311}
312
313pub fn compute_landing_post_index(
323 context: &GitContext,
324 cwd: &Path,
325 input: &PostIndexInput,
326) -> Result<(), GitFailure> {
327 let artifacts = Artifacts {
328 captured: input.captured_index,
329 post: input.post_index,
330 landed: input.landed_index,
331 landed_tree: input.landed_tree,
332 };
333 if input.mode == TransactionMode::ExplicitPath {
334 copy(input.pre_landing_index, input.post_index)?;
335 if !input.committed_paths.is_empty() {
336 let mut arguments: Vec<OsString> = vec!["reset".into(), "--quiet".into(), input.landed_tree.into(), "--".into()];
337 arguments.extend(input.committed_paths.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
338 let mut request = GitRequest::new(cwd, &arguments);
339 request.index = Some(input.post_index.to_path_buf());
340 request.environment = literal_pathspecs();
341 run_git_checked(context, &request)?;
342 }
343 let outside: Vec<Vec<u8>> = input
344 .guarded_paths
345 .iter()
346 .filter(|path| { return !input.committed_paths.contains(path); })
347 .cloned()
348 .collect();
349 return reset_guarded_paths(context, cwd, &artifacts, &outside);
350 }
351 if input.exact_private_index && files_equal(input.pre_landing_index, input.captured_index)
352 .map_err(|error| { return GitFailure(error.0); })?
353 {
354 copy(input.landed_index, input.post_index)?;
355 return Ok(());
356 }
357 warn(TAG, "real index changed since invocation; merging landed entries per path");
358 copy(input.pre_landing_index, input.post_index)?;
359 let mut changed: Vec<Vec<u8>> = changed_paths(context, cwd, input.base_revision, input.landed_tree)?;
360 for path in changed_since_captured(context, cwd, input.captured_index, input.landed_tree)? {
361 if !changed.contains(&path) {
362 changed.push(path);
363 }
364 }
365 return merge_landed_entries(context, cwd, &artifacts, &changed);
366}
367
368fn copy(source: &Path, destination: &Path) -> Result<(), GitFailure> {
378 return copy_index_with_timestamps(source, destination)
379 .map_err(|error| { return GitFailure(format!("copying {} failed: {error}", source.display())); });
380}
381
382#[cfg(test)]
384#[path = "landing_index_tests.rs"]
385mod tests;