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::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 [String],
79 pub pathspec_file: Option<&'selection Path>,
81 pub pathspec_file_nul: bool,
83 pub stage_into_index: bool,
85 pub stage_tracked_changes: bool,
87 pub base_revision: &'selection str,
89}
90
91fn selection_arguments(selection: &IndexSelection) -> Vec<String> {
102 return match selection.pathspec_file {
103 None => {
104 let mut arguments: Vec<String> = vec![String::from("--")];
105 arguments.extend(selection.pathspecs.iter().cloned());
106 arguments
107 }
108 Some(file) => {
109 let mut arguments: Vec<String> =
110 vec![format!("--pathspec-from-file={}", file.display())];
111 if selection.pathspec_file_nul {
112 arguments.push(String::from("--pathspec-file-nul"));
113 }
114 arguments
115 }
116 };
117}
118
119fn private_git(
130 context: &GitContext,
131 cwd: &Path,
132 index: &Path,
133 object_directory: &Path,
134 arguments: &[String],
135) -> Result<GitOutput, GitFailure> {
136 return run_git_checked(
137 context,
138 &indexed_request(cwd, arguments, Some(index), Some(object_directory)),
139 );
140}
141
142pub fn initialize_commit_index(
154 context: &GitContext,
155 cwd: &Path,
156 real_index: &Path,
157 indexes: &CaptureIndexes,
158 selection: &IndexSelection,
159) -> Result<(), GitFailure> {
160 let copy_failure = |error: std::io::Error| { return GitFailure(format!("copying the index failed: {error}")); };
161 copy_index_with_timestamps(real_index, indexes.captured_index.as_path()).map_err(copy_failure)?;
162 if selection.mode == TransactionMode::Index {
163 copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
164 .map_err(copy_failure)?;
165 if selection.stage_tracked_changes {
166 private_git(
168 context,
169 cwd,
170 indexes.commit_index.as_path(),
171 indexes.object_directory.as_path(),
172 &["add".to_string(), "--update".to_string(), "--".to_string(), ":/".to_string()],
173 )?;
174 }
175 if !selection.stage_into_index {
176 return Ok(());
177 }
178 } else {
179 private_git(
180 context,
181 cwd,
182 indexes.commit_index.as_path(),
183 indexes.object_directory.as_path(),
184 &["read-tree".to_string(), selection.base_revision.to_string()],
185 )?;
186 }
187 let mut arguments: Vec<String> = vec![String::from("add"), String::from("--all")];
188 arguments.extend(selection_arguments(selection));
189 private_git(
190 context,
191 cwd,
192 indexes.commit_index.as_path(),
193 indexes.object_directory.as_path(),
194 &arguments,
195 )?;
196 return Ok(());
197}
198
199pub fn write_private_tree(
209 context: &GitContext,
210 cwd: &Path,
211 commit_index: &Path,
212 object_directory: &Path,
213) -> Result<String, GitFailure> {
214 let output: GitOutput = private_git(
215 context,
216 cwd,
217 commit_index,
218 object_directory,
219 &[String::from("write-tree")],
220 )?;
221 let text: &str = std::str::from_utf8(output.stdout.as_slice())
222 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
223 let oid: &str = text.trim();
224 if oid.is_empty() {
225 return Err(GitFailure(String::from("Git returned empty private tree identity.")));
226 }
227 return Ok(String::from(oid));
228}
229
230pub fn list_unmerged_index_paths(
240 context: &GitContext,
241 cwd: &Path,
242 commit_index: &Path,
243) -> Result<Vec<String>, GitFailure> {
244 let output: GitOutput = run_git_checked(
245 context,
246 &indexed_request(cwd, &["ls-files", "--unmerged", "-z"], Some(commit_index), None),
247 )?;
248 let text: &str = std::str::from_utf8(output.stdout.as_slice())
249 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
250 let mut paths: Vec<String> = Vec::new();
251 for record in text.split('\0') {
252 let Some(tab) = record.find('\t') else {
253 continue;
254 };
255 let path: String = String::from(&record[tab + 1..]);
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<String>, 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 let text: &str = std::str::from_utf8(output.stdout.as_slice())
289 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
290 return Ok(text
291 .split('\0')
292 .filter(|path| { return !path.is_empty(); })
293 .map(String::from)
294 .collect());
295}
296
297pub fn materialize_pathspec_file(
308 directory: &Path,
309 effective_cwd: &Path,
310 source: Option<&str>,
311 standard_input: &mut dyn Read,
312) -> std::io::Result<Option<PathBuf>> {
313 let Some(name) = source else {
314 return Ok(None);
315 };
316 if name != "-" {
317 let path: PathBuf = PathBuf::from(name);
318 return Ok(Some(if path.is_absolute() {
319 path
320 } else {
321 effective_cwd.join(path)
322 }));
323 }
324 let mut bytes: Vec<u8> = Vec::new();
325 standard_input.read_to_end(&mut bytes)?;
326 let path: PathBuf = directory.join(PATHSPEC_INPUT_NAME);
327 write_private_file(path.as_path(), bytes.as_slice())?;
328 return Ok(Some(path));
329}
330
331pub fn prepare_interactive_selection(
341 context: &GitContext,
342 cwd: &Path,
343 commit_index: &Path,
344 object_directory: &Path,
345 patch: bool,
346 selection: &IndexSelection,
347) -> Result<(), GitFailure> {
348 let mode: &str = if patch { "--patch" } else { "--interactive" };
349 let mut arguments: Vec<String> = vec![String::from("add"), String::from(mode)];
350 arguments.extend(selection_arguments(selection));
351 let os_arguments: Vec<OsString> = arguments.iter().map(OsString::from).collect();
352 let mut command = git_command(context.real_git.as_path(), os_arguments.as_slice(), context.overlay.as_slice());
353 command
354 .current_dir(cwd)
355 .env("GIT_INDEX_FILE", commit_index)
356 .env("GIT_OBJECT_DIRECTORY", object_directory);
357 let status: std::process::ExitStatus = command
358 .status()
359 .map_err(|error| { return GitFailure(format!("git {} could not start: {error}", arguments.join(" "))); })?;
360 if !status.success() {
361 return Err(GitFailure(format!(
362 "git {} exited {}",
363 arguments.join(" "),
364 status.code().map_or_else(|| { return String::from("by signal"); }, |code| { return code.to_string(); })
365 )));
366 }
367 return Ok(());
368}
369
370#[cfg(test)]
372#[path = "commit_index_tests.rs"]
373mod tests;