1use super::capture_order::{CaptureIndexes, indexed_request};
22use super::start_retry::RetryStart;
23use super::commit_staged_additions::{
25 StagedAddition, carry_additions, explain_ignored_refusal, pathspecs_of_file, staged_additions_selected,
26};
27use super::transaction_git::{GitContext, GitFailure, GitOutput, run_git_checked};
28use super::transaction_journal::TransactionMode;
30use super::private_storage::write_private_file;
32use super::forwarding::git_command;
34use std::io::Read;
36use std::fs::FileTimes;
37use std::ffi::{OsStr, OsString};
39use std::path::{Path, PathBuf};
41
42pub const PATHSPEC_INPUT_NAME: &str = "pathspec.input";
44
45pub fn copy_index_with_timestamps(source: &Path, destination: &Path) -> std::io::Result<()> {
57 std::fs::copy(source, destination)?;
58 let metadata: std::fs::Metadata = std::fs::metadata(source)?;
59 let times: FileTimes = FileTimes::new()
60 .set_accessed(metadata.accessed()?)
61 .set_modified(metadata.modified()?);
62 return std::fs::File::options()
63 .write(true)
64 .open(destination)?
65 .set_times(times);
66}
67
68#[derive(Clone, Copy, Debug)]
79pub struct IndexSelection<'selection> {
80 pub mode: TransactionMode,
82 pub pathspecs: &'selection [OsString],
85 pub pathspec_file: Option<&'selection Path>,
87 pub pathspec_file_nul: bool,
89 pub stage_into_index: bool,
91 pub stage_tracked_changes: bool,
93 pub base_revision: &'selection str,
95}
96
97fn selection_arguments(selection: &IndexSelection) -> Vec<OsString> {
108 return match selection.pathspec_file {
109 None => {
110 let mut arguments: Vec<OsString> = vec![OsString::from("--")];
111 arguments.extend(selection.pathspecs.iter().cloned());
112 arguments
113 }
114 Some(file) => {
115 let mut option: OsString = OsString::from("--pathspec-from-file=");
116 option.push(file.as_os_str());
117 let mut arguments: Vec<OsString> = vec![option];
118 if selection.pathspec_file_nul {
119 arguments.push(OsString::from("--pathspec-file-nul"));
120 }
121 arguments
122 }
123 };
124}
125
126fn private_git<S: AsRef<OsStr>>(
137 context: &GitContext,
138 cwd: &Path,
139 index: &Path,
140 object_directory: &Path,
141 arguments: &[S],
142) -> Result<GitOutput, GitFailure> {
143 return run_git_checked(
144 context,
145 &indexed_request(cwd, arguments, Some(index), Some(object_directory)),
146 );
147}
148
149pub fn initialize_commit_index(
161 context: &GitContext,
162 cwd: &Path,
163 real_index: &Path,
164 indexes: &CaptureIndexes,
165 selection: &IndexSelection,
166) -> Result<(), GitFailure> {
167 let copy_failure = |error: std::io::Error| { return GitFailure(format!("copying the index failed: {error}")); };
168 if !real_index.exists() {
170 run_git_checked(
171 context,
172 &indexed_request(cwd, &["read-tree".to_string(), "--empty".to_string()], Some(real_index), None),
173 )?;
174 }
175 copy_index_with_timestamps(real_index, indexes.captured_index.as_path()).map_err(copy_failure)?;
176 if selection.mode == TransactionMode::Index {
177 copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
178 .map_err(copy_failure)?;
179 if selection.stage_tracked_changes {
180 private_git(
182 context,
183 cwd,
184 indexes.commit_index.as_path(),
185 indexes.object_directory.as_path(),
186 &["add".to_string(), "--update".to_string(), "--".to_string(), ":/".to_string()],
187 )?;
188 }
189 if !selection.stage_into_index {
190 return Ok(());
191 }
192 } else {
193 private_git(
194 context,
195 cwd,
196 indexes.commit_index.as_path(),
197 indexes.object_directory.as_path(),
198 &["read-tree".to_string(), selection.base_revision.to_string()],
199 )?;
200 carry_selected_staged_additions(context, cwd, indexes, selection)?;
201 }
202 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from("--all")];
203 arguments.extend(selection_arguments(selection));
204 private_git(
205 context,
206 cwd,
207 indexes.commit_index.as_path(),
208 indexes.object_directory.as_path(),
209 &arguments,
210 )
211 .map_err(explain_ignored_refusal)?;
212 return Ok(());
213}
214
215fn carry_selected_staged_additions(
227 context: &GitContext,
228 cwd: &Path,
229 indexes: &CaptureIndexes,
230 selection: &IndexSelection,
231) -> Result<(), GitFailure> {
232 let mut pathspecs: Vec<OsString> = selection.pathspecs.to_vec();
233 if let Some(file) = selection.pathspec_file {
234 let Ok(bytes) = std::fs::read(file) else {
236 return Ok(());
237 };
238 let Some(listed) = pathspecs_of_file(bytes.as_slice(), selection.pathspec_file_nul) else {
239 return Ok(());
240 };
241 pathspecs.extend(listed);
242 }
243 let additions: Vec<StagedAddition> = staged_additions_selected(
244 context,
245 cwd,
246 indexes.captured_index.as_path(),
247 selection.base_revision,
248 pathspecs.as_slice(),
249 )?;
250 return carry_additions(
251 context,
252 cwd,
253 indexes.commit_index.as_path(),
254 indexes.object_directory.as_path(),
255 additions.as_slice(),
256 );
257}
258
259pub fn write_private_tree(
269 context: &GitContext,
270 cwd: &Path,
271 commit_index: &Path,
272 object_directory: &Path,
273) -> Result<String, GitFailure> {
274 let output: GitOutput = private_git(
275 context,
276 cwd,
277 commit_index,
278 object_directory,
279 &[String::from("write-tree")],
280 )?;
281 let text: &str = std::str::from_utf8(output.stdout.as_slice())
282 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
283 let oid: &str = text.trim();
284 if oid.is_empty() {
285 return Err(GitFailure(String::from("Git returned empty private tree identity.")));
286 }
287 return Ok(String::from(oid));
288}
289
290pub fn list_unmerged_index_paths(
300 context: &GitContext,
301 cwd: &Path,
302 commit_index: &Path,
303) -> Result<Vec<Vec<u8>>, GitFailure> {
304 let output: GitOutput = run_git_checked(
305 context,
306 &indexed_request(cwd, &["ls-files", "--unmerged", "-z"], Some(commit_index), None),
307 )?;
308 let mut paths: Vec<Vec<u8>> = Vec::new();
309 for record in output.stdout.split(|byte| { return *byte == 0; }) {
310 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
311 continue;
312 };
313 let path: Vec<u8> = record[tab + 1..].to_vec();
314 if !paths.contains(&path) {
315 paths.push(path);
316 }
317 }
318 return Ok(paths);
319}
320
321pub fn list_changed_index_paths(
331 context: &GitContext,
332 cwd: &Path,
333 commit_index: &Path,
334 base_revision: &str,
335 object_directory: &Path,
336) -> Result<Vec<Vec<u8>>, GitFailure> {
337 let output: GitOutput = run_git_checked(
338 context,
339 &indexed_request(
340 cwd,
341 &["diff", "--cached", "--name-only", "-z", base_revision],
342 Some(commit_index),
343 Some(object_directory),
344 ),
345 )?;
346 return Ok(output
347 .stdout
348 .split(|byte| { return *byte == 0; })
349 .filter(|path| { return !path.is_empty(); })
350 .map(<[u8]>::to_vec)
351 .collect());
352}
353
354pub fn materialize_pathspec_file(
365 directory: &Path,
366 effective_cwd: &Path,
367 source: Option<&str>,
368 standard_input: &mut dyn Read,
369) -> std::io::Result<Option<PathBuf>> {
370 let Some(name) = source else {
371 return Ok(None);
372 };
373 if name != "-" {
374 let path: PathBuf = PathBuf::from(name);
375 return Ok(Some(if path.is_absolute() {
376 path
377 } else {
378 effective_cwd.join(path)
379 }));
380 }
381 let mut bytes: Vec<u8> = Vec::new();
382 standard_input.read_to_end(&mut bytes)?;
383 let path: PathBuf = directory.join(PATHSPEC_INPUT_NAME);
384 write_private_file(path.as_path(), bytes.as_slice())?;
385 return Ok(Some(path));
386}
387
388pub fn prepare_interactive_selection(
398 context: &GitContext,
399 cwd: &Path,
400 commit_index: &Path,
401 object_directory: &Path,
402 patch: bool,
403 selection: &IndexSelection,
404) -> Result<(), GitFailure> {
405 let mode: &str = if patch { "--patch" } else { "--interactive" };
406 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from(mode)];
407 arguments.extend(selection_arguments(selection));
408 let shown: String = arguments
410 .iter()
411 .map(|argument| { return argument.to_string_lossy().into_owned(); })
412 .collect::<Vec<String>>()
413 .join(" ");
414 let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
415 command
416 .current_dir(cwd)
417 .env("GIT_INDEX_FILE", commit_index)
418 .env("GIT_OBJECT_DIRECTORY", object_directory);
419 let status: std::process::ExitStatus = command
420 .status_retrying()
421 .map_err(|error| { return GitFailure(format!("git {shown} could not start: {error}")); })?;
422 if !status.success() {
423 return Err(GitFailure(format!(
424 "git {shown} exited {}",
425 status.code().map_or_else(|| { return String::from("by signal"); }, |code| { return code.to_string(); })
426 )));
427 }
428 return Ok(());
429}
430
431#[cfg(test)]
433#[path = "commit_index_tests.rs"]
434mod tests;