1use super::commit_index::copy_index_with_timestamps;
18use super::recovery_files::files_equal;
20use super::diagnostic_log::warn;
22use super::landing_invariant::warn_when_entries_moved;
23use super::path_bytes::{os_from_path_bytes, path_message_text};
25use super::transaction_journal::TransactionMode;
27use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git, run_git_checked};
29use std::collections::HashMap;
31use std::ffi::OsString;
33use std::path::Path;
34
35const TAG: &str = "computeLandingPostIndex";
37
38pub struct PostIndexInput<'input> {
48 pub mode: TransactionMode,
50 pub pre_landing_index: &'input Path,
52 pub captured_index: &'input Path,
54 pub landed_index: &'input Path,
56 pub post_index: &'input Path,
58 pub landed_tree: &'input str,
60 pub base_revision: &'input str,
62 pub committed_paths: &'input [Vec<u8>],
64 pub guarded_paths: &'input [Vec<u8>],
66 pub exact_private_index: bool,
68}
69
70fn literal_pathspecs() -> Vec<(OsString, OsString)> {
80 return vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
81}
82
83fn literal_pathspec(path: &[u8]) -> OsString {
93 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
94 pathspec.extend_from_slice(path);
95 return os_from_path_bytes(pathspec.as_slice());
96}
97
98fn records(bytes: &[u8]) -> Vec<Vec<u8>> {
108 return bytes
109 .split(|byte| { return *byte == 0; })
110 .filter(|record| { return !record.is_empty(); })
111 .map(<[u8]>::to_vec)
112 .collect();
113}
114
115fn list_stages(
125 context: &GitContext,
126 cwd: &Path,
127 index: &Path,
128 paths: &[Vec<u8>],
129) -> Result<HashMap<Vec<u8>, Vec<Vec<u8>>>, GitFailure> {
130 let mut grouped: HashMap<Vec<u8>, Vec<Vec<u8>>> = HashMap::new();
131 if paths.is_empty() {
132 return Ok(grouped);
133 }
134 let mut arguments: Vec<OsString> = ["ls-files", "--stage", "-z", "--full-name", "--"]
135 .iter()
136 .map(OsString::from)
137 .collect();
138 arguments.extend(paths.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
139 let mut request = GitRequest::new(cwd, &arguments);
140 request.index = Some(index.to_path_buf());
141 request.environment = literal_pathspecs();
142 let output: GitOutput = run_git_checked(context, &request)?;
143 for record in records(&output.stdout) {
144 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
145 return Err(GitFailure(format!("ls-files record has no path: {}", path_message_text(record.as_slice()))));
146 };
147 grouped.entry(record[tab + 1..].to_vec()).or_default().push(record.clone());
148 }
149 return Ok(grouped);
150}
151
152fn changed_paths(context: &GitContext, cwd: &Path, from: &str, to: &str) -> Result<Vec<Vec<u8>>, GitFailure> {
162 let request = GitRequest::new(cwd, &["diff-tree", "-r", "--no-renames", "--name-only", "-z", from, to]);
163 let output: GitOutput = run_git_checked(context, &request)?;
164 return Ok(records(&output.stdout));
165}
166
167fn changed_since_captured(
177 context: &GitContext,
178 cwd: &Path,
179 captured: &Path,
180 landed_tree: &str,
181) -> Result<Vec<Vec<u8>>, GitFailure> {
182 let mut request = GitRequest::new(cwd, &["write-tree"]);
183 request.index = Some(captured.to_path_buf());
184 let output: GitOutput = run_git(context, &request)
185 .map_err(|error| { return GitFailure(format!("git write-tree could not start: {error}")); })?;
186 if !output.succeeded() {
187 return Ok(Vec::new());
188 }
189 let tree: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
190 return changed_paths(context, cwd, tree.as_str(), landed_tree);
191}
192
193fn merge_landed_entries(
204 context: &GitContext,
205 cwd: &Path,
206 artifacts: &Artifacts,
207 changed: &[Vec<u8>],
208) -> Result<(), GitFailure> {
209 let captured = list_stages(context, cwd, artifacts.captured, changed)?;
210 let current = list_stages(context, cwd, artifacts.post, changed)?;
211 let landed = list_stages(context, cwd, artifacts.landed, changed)?;
212 let untouched: Vec<&Vec<u8>> = changed
213 .iter()
214 .filter(|path| {
215 return captured.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[])
216 == current.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
217 })
218 .collect();
219 if untouched.is_empty() {
220 return Ok(());
221 }
222 let zero: String = "0".repeat(artifacts.landed_tree.len());
223 let mut body: Vec<u8> = Vec::new();
224 for path in untouched {
225 match landed.get(path.as_slice()) {
226 Some(entries) => {
227 for entry in entries {
228 body.extend_from_slice(entry.as_slice());
229 body.push(0);
230 }
231 }
232 None => {
233 body.extend_from_slice(format!("0 {zero}\t").as_bytes());
234 body.extend_from_slice(path.as_slice());
235 body.push(0);
236 }
237 }
238 }
239 let mut request = GitRequest::new(cwd, &["update-index", "-z", "--index-info"]);
240 request.index = Some(artifacts.post.to_path_buf());
241 request.input = Some(body);
242 run_git_checked(context, &request)?;
243 return Ok(());
244}
245
246struct Artifacts<'artifacts> {
256 captured: &'artifacts Path,
258 post: &'artifacts Path,
260 landed: &'artifacts Path,
262 landed_tree: &'artifacts str,
264}
265
266fn reset_guarded_paths(
276 context: &GitContext,
277 cwd: &Path,
278 artifacts: &Artifacts,
279 guarded: &[Vec<u8>],
280) -> Result<(), GitFailure> {
281 if guarded.is_empty() {
282 return Ok(());
283 }
284 let captured = list_stages(context, cwd, artifacts.captured, guarded)?;
285 let current = list_stages(context, cwd, artifacts.post, guarded)?;
286 let mut untouched: Vec<&Vec<u8>> = Vec::new();
287 for path in guarded {
288 let before: &[Vec<u8>] = captured.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
289 let now: &[Vec<u8>] = current.get(path.as_slice()).map(Vec::as_slice).unwrap_or(&[]);
290 if before == now {
291 untouched.push(path);
292 } else {
293 let shown: String = path_message_text(path.as_slice());
294 warn(
295 TAG,
296 &format!(
297 "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}."
298 ),
299 );
300 }
301 }
302 if untouched.is_empty() {
303 return Ok(());
304 }
305 let mut arguments: Vec<OsString> = vec!["reset".into(), "--quiet".into(), artifacts.landed_tree.into(), "--".into()];
306 arguments.extend(untouched.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
307 let mut request = GitRequest::new(cwd, &arguments);
308 request.index = Some(artifacts.post.to_path_buf());
309 request.environment = literal_pathspecs();
310 run_git_checked(context, &request)?;
311 return Ok(());
312}
313
314pub fn compute_landing_post_index(
324 context: &GitContext,
325 cwd: &Path,
326 input: &PostIndexInput,
327) -> Result<(), GitFailure> {
328 let artifacts = Artifacts {
329 captured: input.captured_index,
330 post: input.post_index,
331 landed: input.landed_index,
332 landed_tree: input.landed_tree,
333 };
334 if input.mode == TransactionMode::ExplicitPath {
335 copy(input.pre_landing_index, input.post_index)?;
336 if !input.committed_paths.is_empty() {
337 let mut arguments: Vec<OsString> = vec!["reset".into(), "--quiet".into(), input.landed_tree.into(), "--".into()];
338 arguments.extend(input.committed_paths.iter().map(|path| { return literal_pathspec(path.as_slice()); }));
339 let mut request = GitRequest::new(cwd, &arguments);
340 request.index = Some(input.post_index.to_path_buf());
341 request.environment = literal_pathspecs();
342 run_git_checked(context, &request)?;
343 }
344 let outside: Vec<Vec<u8>> = input
345 .guarded_paths
346 .iter()
347 .filter(|path| { return !input.committed_paths.contains(path); })
348 .cloned()
349 .collect();
350 reset_guarded_paths(context, cwd, &artifacts, &outside)?;
351 let mut allowed: Vec<Vec<u8>> = input.committed_paths.to_vec();
353 allowed.extend(input.guarded_paths.iter().cloned());
354 warn_when_entries_moved(context, cwd, input.pre_landing_index, input.post_index, &allowed);
355 return Ok(());
356 }
357 if input.exact_private_index && files_equal(input.pre_landing_index, input.captured_index)
358 .map_err(|error| { return GitFailure(error.0); })?
359 {
360 copy(input.landed_index, input.post_index)?;
361 return Ok(());
362 }
363 warn(TAG, "real index changed since invocation; merging landed entries per path");
364 copy(input.pre_landing_index, input.post_index)?;
365 let mut changed: Vec<Vec<u8>> = changed_paths(context, cwd, input.base_revision, input.landed_tree)?;
366 for path in changed_since_captured(context, cwd, input.captured_index, input.landed_tree)? {
367 if !changed.contains(&path) {
368 changed.push(path);
369 }
370 }
371 return merge_landed_entries(context, cwd, &artifacts, &changed);
372}
373
374fn copy(source: &Path, destination: &Path) -> Result<(), GitFailure> {
384 return copy_index_with_timestamps(source, destination)
385 .map_err(|error| { return GitFailure(format!("copying {} failed: {error}", source.display())); });
386}
387
388#[cfg(test)]
390#[path = "landing_index_tests.rs"]
391mod tests;