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 if !real_index.exists() {
165 run_git_checked(
166 context,
167 &indexed_request(cwd, &["read-tree".to_string(), "--empty".to_string()], Some(real_index), None),
168 )?;
169 }
170 copy_index_with_timestamps(real_index, indexes.captured_index.as_path()).map_err(copy_failure)?;
171 if selection.mode == TransactionMode::Index {
172 copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
173 .map_err(copy_failure)?;
174 if selection.stage_tracked_changes {
175 private_git(
177 context,
178 cwd,
179 indexes.commit_index.as_path(),
180 indexes.object_directory.as_path(),
181 &["add".to_string(), "--update".to_string(), "--".to_string(), ":/".to_string()],
182 )?;
183 }
184 if !selection.stage_into_index {
185 return Ok(());
186 }
187 } else {
188 private_git(
189 context,
190 cwd,
191 indexes.commit_index.as_path(),
192 indexes.object_directory.as_path(),
193 &["read-tree".to_string(), selection.base_revision.to_string()],
194 )?;
195 }
196 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from("--all")];
197 arguments.extend(selection_arguments(selection));
198 private_git(
199 context,
200 cwd,
201 indexes.commit_index.as_path(),
202 indexes.object_directory.as_path(),
203 &arguments,
204 )?;
205 return Ok(());
206}
207
208pub fn write_private_tree(
218 context: &GitContext,
219 cwd: &Path,
220 commit_index: &Path,
221 object_directory: &Path,
222) -> Result<String, GitFailure> {
223 let output: GitOutput = private_git(
224 context,
225 cwd,
226 commit_index,
227 object_directory,
228 &[String::from("write-tree")],
229 )?;
230 let text: &str = std::str::from_utf8(output.stdout.as_slice())
231 .map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
232 let oid: &str = text.trim();
233 if oid.is_empty() {
234 return Err(GitFailure(String::from("Git returned empty private tree identity.")));
235 }
236 return Ok(String::from(oid));
237}
238
239pub fn list_unmerged_index_paths(
249 context: &GitContext,
250 cwd: &Path,
251 commit_index: &Path,
252) -> Result<Vec<Vec<u8>>, GitFailure> {
253 let output: GitOutput = run_git_checked(
254 context,
255 &indexed_request(cwd, &["ls-files", "--unmerged", "-z"], Some(commit_index), None),
256 )?;
257 let mut paths: Vec<Vec<u8>> = Vec::new();
258 for record in output.stdout.split(|byte| { return *byte == 0; }) {
259 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
260 continue;
261 };
262 let path: Vec<u8> = record[tab + 1..].to_vec();
263 if !paths.contains(&path) {
264 paths.push(path);
265 }
266 }
267 return Ok(paths);
268}
269
270pub fn list_changed_index_paths(
280 context: &GitContext,
281 cwd: &Path,
282 commit_index: &Path,
283 base_revision: &str,
284 object_directory: &Path,
285) -> Result<Vec<Vec<u8>>, GitFailure> {
286 let output: GitOutput = run_git_checked(
287 context,
288 &indexed_request(
289 cwd,
290 &["diff", "--cached", "--name-only", "-z", base_revision],
291 Some(commit_index),
292 Some(object_directory),
293 ),
294 )?;
295 return Ok(output
296 .stdout
297 .split(|byte| { return *byte == 0; })
298 .filter(|path| { return !path.is_empty(); })
299 .map(<[u8]>::to_vec)
300 .collect());
301}
302
303pub fn materialize_pathspec_file(
314 directory: &Path,
315 effective_cwd: &Path,
316 source: Option<&str>,
317 standard_input: &mut dyn Read,
318) -> std::io::Result<Option<PathBuf>> {
319 let Some(name) = source else {
320 return Ok(None);
321 };
322 if name != "-" {
323 let path: PathBuf = PathBuf::from(name);
324 return Ok(Some(if path.is_absolute() {
325 path
326 } else {
327 effective_cwd.join(path)
328 }));
329 }
330 let mut bytes: Vec<u8> = Vec::new();
331 standard_input.read_to_end(&mut bytes)?;
332 let path: PathBuf = directory.join(PATHSPEC_INPUT_NAME);
333 write_private_file(path.as_path(), bytes.as_slice())?;
334 return Ok(Some(path));
335}
336
337pub fn prepare_interactive_selection(
347 context: &GitContext,
348 cwd: &Path,
349 commit_index: &Path,
350 object_directory: &Path,
351 patch: bool,
352 selection: &IndexSelection,
353) -> Result<(), GitFailure> {
354 let mode: &str = if patch { "--patch" } else { "--interactive" };
355 let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from(mode)];
356 arguments.extend(selection_arguments(selection));
357 let shown: String = arguments
359 .iter()
360 .map(|argument| { return argument.to_string_lossy().into_owned(); })
361 .collect::<Vec<String>>()
362 .join(" ");
363 let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
364 command
365 .current_dir(cwd)
366 .env("GIT_INDEX_FILE", commit_index)
367 .env("GIT_OBJECT_DIRECTORY", object_directory);
368 let status: std::process::ExitStatus = command
369 .status()
370 .map_err(|error| { return GitFailure(format!("git {shown} could not start: {error}")); })?;
371 if !status.success() {
372 return Err(GitFailure(format!(
373 "git {shown} exited {}",
374 status.code().map_or_else(|| { return String::from("by signal"); }, |code| { return code.to_string(); })
375 )));
376 }
377 return Ok(());
378}
379
380#[cfg(test)]
382#[path = "commit_index_tests.rs"]
383mod tests;