1use super::capture_order::{CaptureIndexes, indexed_request};
22use super::transaction_git::{GitContext, GitFailure, GitOutput, run_git_checked};
23use super::transaction_journal::TransactionMode;
25use super::private_storage::write_private_file;
27use super::forwarding::git_command;
29use std::io::Read;
31use std::fs::FileTimes;
32use std::ffi::{OsStr, OsString};
34use std::path::{Path, PathBuf};
36
37pub const PATHSPEC_INPUT_NAME: &str = "pathspec.input";
39
40pub fn copy_index_with_timestamps(source: &Path, destination: &Path) -> std::io::Result<()> {
52 std::fs::copy(source, destination)?;
53 let metadata: std::fs::Metadata = std::fs::metadata(source)?;
54 let times: FileTimes = FileTimes::new()
55 .set_accessed(metadata.accessed()?)
56 .set_modified(metadata.modified()?);
57 return std::fs::File::options()
58 .write(true)
59 .open(destination)?
60 .set_times(times);
61}
62
63#[derive(Clone, Copy, Debug)]
74pub struct IndexSelection<'selection> {
75 pub mode: TransactionMode,
77 pub pathspecs: &'selection [OsString],
80 pub pathspec_file: Option<&'selection Path>,
82 pub pathspec_file_nul: bool,
84 pub stage_into_index: bool,
86 pub stage_tracked_changes: bool,
88 pub base_revision: &'selection str,
90}
91
92fn selection_arguments(selection: &IndexSelection) -> Vec<OsString> {
103 return match selection.pathspec_file {
104 None => {
105 let mut arguments: Vec<OsString> = vec![OsString::from("--")];
106 arguments.extend(selection.pathspecs.iter().cloned());
107 arguments
108 }
109 Some(file) => {
110 let mut option: OsString = OsString::from("--pathspec-from-file=");
111 option.push(file.as_os_str());
112 let mut arguments: Vec<OsString> = vec![option];
113 if selection.pathspec_file_nul {
114 arguments.push(OsString::from("--pathspec-file-nul"));
115 }
116 arguments
117 }
118 };
119}
120
121fn private_git<S: AsRef<OsStr>>(
132 context: &GitContext,
133 cwd: &Path,
134 index: &Path,
135 object_directory: &Path,
136 arguments: &[S],
137) -> Result<GitOutput, GitFailure> {
138 return run_git_checked(
139 context,
140 &indexed_request(cwd, arguments, Some(index), Some(object_directory)),
141 );
142}
143
144pub fn initialize_commit_index(
156 context: &GitContext,
157 cwd: &Path,
158 real_index: &Path,
159 indexes: &CaptureIndexes,
160 selection: &IndexSelection,
161) -> Result<(), GitFailure> {
162 let copy_failure = |error: std::io::Error| { return GitFailure(format!("copying the index failed: {error}")); };
163 copy_index_with_timestamps(real_index, indexes.captured_index.as_path()).map_err(copy_failure)?;
164 if selection.mode == TransactionMode::Index {
165 copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
166 .map_err(copy_failure)?;
167 if selection.stage_tracked_changes {
168 private_git(
170 context,
171 cwd,
172 indexes.commit_index.as_path(),
173 indexes.object_directory.as_path(),
174 &["add".to_string(), "--update".to_string(), "--".to_string(), ":/".to_string()],
175 )?;
176 }
177 if !selection.stage_into_index {
178 return Ok(());
179 }
180 } else {
181 private_git(
182 context,
183 cwd,
184 indexes.commit_index.as_path(),
185 indexes.object_directory.as_path(),
186 &["read-tree".to_string(), selection.base_revision.to_string()],
187 )?;
188 }
189 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from("--all")];
190 arguments.extend(selection_arguments(selection));
191 private_git(
192 context,
193 cwd,
194 indexes.commit_index.as_path(),
195 indexes.object_directory.as_path(),
196 &arguments,
197 )?;
198 return Ok(());
199}
200
201pub fn write_private_tree(
211 context: &GitContext,
212 cwd: &Path,
213 commit_index: &Path,
214 object_directory: &Path,
215) -> Result<String, GitFailure> {
216 let output: GitOutput = private_git(
217 context,
218 cwd,
219 commit_index,
220 object_directory,
221 &[String::from("write-tree")],
222 )?;
223 let text: &str = std::str::from_utf8(output.stdout.as_slice())
224 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
225 let oid: &str = text.trim();
226 if oid.is_empty() {
227 return Err(GitFailure(String::from("Git returned empty private tree identity.")));
228 }
229 return Ok(String::from(oid));
230}
231
232pub fn list_unmerged_index_paths(
242 context: &GitContext,
243 cwd: &Path,
244 commit_index: &Path,
245) -> Result<Vec<Vec<u8>>, GitFailure> {
246 let output: GitOutput = run_git_checked(
247 context,
248 &indexed_request(cwd, &["ls-files", "--unmerged", "-z"], Some(commit_index), None),
249 )?;
250 let mut paths: Vec<Vec<u8>> = Vec::new();
251 for record in output.stdout.split(|byte| { return *byte == 0; }) {
252 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
253 continue;
254 };
255 let path: Vec<u8> = record[tab + 1..].to_vec();
256 if !paths.contains(&path) {
257 paths.push(path);
258 }
259 }
260 return Ok(paths);
261}
262
263pub fn list_changed_index_paths(
273 context: &GitContext,
274 cwd: &Path,
275 commit_index: &Path,
276 base_revision: &str,
277 object_directory: &Path,
278) -> Result<Vec<Vec<u8>>, GitFailure> {
279 let output: GitOutput = run_git_checked(
280 context,
281 &indexed_request(
282 cwd,
283 &["diff", "--cached", "--name-only", "-z", base_revision],
284 Some(commit_index),
285 Some(object_directory),
286 ),
287 )?;
288 return Ok(output
289 .stdout
290 .split(|byte| { return *byte == 0; })
291 .filter(|path| { return !path.is_empty(); })
292 .map(<[u8]>::to_vec)
293 .collect());
294}
295
296pub fn materialize_pathspec_file(
307 directory: &Path,
308 effective_cwd: &Path,
309 source: Option<&str>,
310 standard_input: &mut dyn Read,
311) -> std::io::Result<Option<PathBuf>> {
312 let Some(name) = source else {
313 return Ok(None);
314 };
315 if name != "-" {
316 let path: PathBuf = PathBuf::from(name);
317 return Ok(Some(if path.is_absolute() {
318 path
319 } else {
320 effective_cwd.join(path)
321 }));
322 }
323 let mut bytes: Vec<u8> = Vec::new();
324 standard_input.read_to_end(&mut bytes)?;
325 let path: PathBuf = directory.join(PATHSPEC_INPUT_NAME);
326 write_private_file(path.as_path(), bytes.as_slice())?;
327 return Ok(Some(path));
328}
329
330pub fn prepare_interactive_selection(
340 context: &GitContext,
341 cwd: &Path,
342 commit_index: &Path,
343 object_directory: &Path,
344 patch: bool,
345 selection: &IndexSelection,
346) -> Result<(), GitFailure> {
347 let mode: &str = if patch { "--patch" } else { "--interactive" };
348 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from(mode)];
349 arguments.extend(selection_arguments(selection));
350 let shown: String = arguments
352 .iter()
353 .map(|argument| { return argument.to_string_lossy().into_owned(); })
354 .collect::<Vec<String>>()
355 .join(" ");
356 let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
357 command
358 .current_dir(cwd)
359 .env("GIT_INDEX_FILE", commit_index)
360 .env("GIT_OBJECT_DIRECTORY", object_directory);
361 let status: std::process::ExitStatus = command
362 .status()
363 .map_err(|error| { return GitFailure(format!("git {shown} could not start: {error}")); })?;
364 if !status.success() {
365 return Err(GitFailure(format!(
366 "git {shown} exited {}",
367 status.code().map_or_else(|| { return String::from("by signal"); }, |code| { return code.to_string(); })
368 )));
369 }
370 return Ok(());
371}
372
373#[cfg(test)]
375#[path = "commit_index_tests.rs"]
376mod tests;