use crate::private_index_request::{CaptureIndexes, indexed_request};
use crate::start_retry::RetryStart;
use crate::commit_staged_additions::{
StagedAddition, carry_additions, explain_ignored_refusal, pathspecs_of_file, staged_additions_selected,
};
use crate::transaction_git::{GitContext, GitFailure, GitOutput, run_git_checked};
use crate::transaction_journal::TransactionMode;
use crate::index_flags::{IndexFlags, read_index_flags};
use crate::forwarding::git_command;
use std::fs::FileTimes;
use std::ffi::{OsStr, OsString};
use std::path::Path;
pub const PATHSPEC_INPUT_NAME: &str = "pathspec.input";
pub fn copy_index_with_timestamps(source: &Path, destination: &Path) -> std::io::Result<()> {
let mut opened: std::fs::File = std::fs::File::open(source)?;
let metadata: std::fs::Metadata = opened.metadata()?;
let times: FileTimes = FileTimes::new()
.set_accessed(metadata.accessed()?)
.set_modified(metadata.modified()?);
let mut copy: std::fs::File = std::fs::File::create(destination)?;
std::io::copy(&mut opened, &mut copy)?;
copy.set_permissions(metadata.permissions())?;
return copy.set_times(times);
}
fn seeds_private_index(captured_index: &Path) -> bool {
let Ok(bytes) = std::fs::read(captured_index) else {
return false;
};
return read_index_flags(bytes.as_slice()) == IndexFlags::Plain;
}
#[derive(Clone, Copy, Debug)]
pub struct IndexSelection<'selection> {
pub mode: TransactionMode,
pub pathspecs: &'selection [OsString],
pub pathspec_file: Option<&'selection Path>,
pub pathspec_file_nul: bool,
pub stage_into_index: bool,
pub stage_tracked_changes: bool,
pub base_revision: &'selection str,
}
fn selection_arguments(selection: &IndexSelection) -> Vec<OsString> {
return match selection.pathspec_file {
None => {
let mut arguments: Vec<OsString> = vec![OsString::from("--")];
arguments.extend(selection.pathspecs.iter().cloned());
arguments
}
Some(file) => {
let mut option: OsString = OsString::from("--pathspec-from-file=");
option.push(file.as_os_str());
let mut arguments: Vec<OsString> = vec![option];
if selection.pathspec_file_nul {
arguments.push(OsString::from("--pathspec-file-nul"));
}
arguments
}
};
}
fn private_git<S: AsRef<OsStr>>(
context: &GitContext,
cwd: &Path,
index: &Path,
object_directory: &Path,
arguments: &[S],
) -> Result<GitOutput, GitFailure> {
return run_git_checked(
context,
&indexed_request(cwd, arguments, Some(index), Some(object_directory)),
);
}
pub fn initialize_commit_index(
context: &GitContext,
cwd: &Path,
real_index: &Path,
indexes: &CaptureIndexes,
selection: &IndexSelection,
) -> Result<(), GitFailure> {
let copy_failure = |error: std::io::Error| { return GitFailure(format!("copying the index failed: {error}")); };
if !real_index.exists() {
run_git_checked(
context,
&indexed_request(cwd, &["read-tree".to_string(), "--empty".to_string()], Some(real_index), None),
)?;
}
copy_index_with_timestamps(real_index, indexes.captured_index.as_path()).map_err(copy_failure)?;
if selection.mode == TransactionMode::Index {
copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
.map_err(copy_failure)?;
if selection.stage_tracked_changes {
private_git(
context,
cwd,
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
&["add".to_string(), "--update".to_string(), "--".to_string(), ":/".to_string()],
)?;
}
if !selection.stage_into_index {
return Ok(());
}
} else {
let seeded: bool = seeds_private_index(indexes.captured_index.as_path());
if seeded {
copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
.map_err(copy_failure)?;
}
if seeded && index_equals_base(context, cwd, indexes, selection.base_revision) {
return add_selected_paths(context, cwd, indexes, selection);
}
let mut read_tree: Vec<String> = vec![String::from("read-tree")];
if seeded {
read_tree.push(String::from("--reset"));
}
read_tree.push(selection.base_revision.to_string());
private_git(
context,
cwd,
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
&read_tree,
)?;
carry_selected_staged_additions(context, cwd, indexes, selection)?;
}
return add_selected_paths(context, cwd, indexes, selection);
}
fn add_selected_paths(
context: &GitContext,
cwd: &Path,
indexes: &CaptureIndexes,
selection: &IndexSelection,
) -> Result<(), GitFailure> {
let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from("--all")];
arguments.extend(selection_arguments(selection));
private_git(
context,
cwd,
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
&arguments,
)
.map_err(explain_ignored_refusal)?;
return Ok(());
}
fn index_equals_base(context: &GitContext, cwd: &Path, indexes: &CaptureIndexes, base_revision: &str) -> bool {
let request = indexed_request(
cwd,
&["diff-index", "--cached", "--name-only", "-z", "--no-renames", base_revision],
Some(indexes.captured_index.as_path()),
Some(indexes.object_directory.as_path()),
);
return crate::repo_read_hook::try_request(context, &request)
.is_some_and(|answer| { return answer.code == Some(0) && answer.stdout.is_empty(); });
}
fn carry_selected_staged_additions(
context: &GitContext,
cwd: &Path,
indexes: &CaptureIndexes,
selection: &IndexSelection,
) -> Result<(), GitFailure> {
let mut pathspecs: Vec<OsString> = selection.pathspecs.to_vec();
if let Some(file) = selection.pathspec_file {
let Ok(bytes) = std::fs::read(file) else {
return Ok(());
};
let Some(listed) = pathspecs_of_file(bytes.as_slice(), selection.pathspec_file_nul) else {
return Ok(());
};
pathspecs.extend(listed);
}
let additions: Vec<StagedAddition> = staged_additions_selected(
context,
cwd,
indexes.captured_index.as_path(),
selection.base_revision,
pathspecs.as_slice(),
)?;
return carry_additions(
context,
cwd,
indexes.commit_index.as_path(),
indexes.object_directory.as_path(),
additions.as_slice(),
);
}
pub fn write_private_tree(
context: &GitContext,
cwd: &Path,
commit_index: &Path,
object_directory: &Path,
) -> Result<String, GitFailure> {
let output: GitOutput = private_git(
context,
cwd,
commit_index,
object_directory,
&[String::from("write-tree")],
)?;
let text: &str = std::str::from_utf8(output.stdout.as_slice())
.map_err(|_| { return GitFailure(String::from("Git returned non-UTF-8 output.")); })?;
let oid: &str = text.trim();
if oid.is_empty() {
return Err(GitFailure(String::from("Git returned empty private tree identity.")));
}
return Ok(String::from(oid));
}
pub fn list_unmerged_index_paths(
context: &GitContext,
cwd: &Path,
commit_index: &Path,
) -> Result<Vec<Vec<u8>>, GitFailure> {
let output: GitOutput = run_git_checked(
context,
&indexed_request(cwd, &["ls-files", "--unmerged", "-z"], Some(commit_index), None),
)?;
let mut paths: Vec<Vec<u8>> = Vec::new();
for record in output.stdout.split(|byte| { return *byte == 0; }) {
let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
continue;
};
let path: Vec<u8> = record[tab + 1..].to_vec();
if !paths.contains(&path) {
paths.push(path);
}
}
return Ok(paths);
}
pub fn list_changed_index_paths(
context: &GitContext,
cwd: &Path,
commit_index: &Path,
base_revision: &str,
object_directory: &Path,
) -> Result<Vec<Vec<u8>>, GitFailure> {
let output: GitOutput = run_git_checked(
context,
&indexed_request(
cwd,
&["diff", "--cached", "--name-only", "-z", base_revision],
Some(commit_index),
Some(object_directory),
),
)?;
return Ok(output
.stdout
.split(|byte| { return *byte == 0; })
.filter(|path| { return !path.is_empty(); })
.map(<[u8]>::to_vec)
.collect());
}
pub fn prepare_interactive_selection(
context: &GitContext,
cwd: &Path,
commit_index: &Path,
object_directory: &Path,
patch: bool,
selection: &IndexSelection,
) -> Result<(), GitFailure> {
let mode: &str = if patch { "--patch" } else { "--interactive" };
let mut arguments: Vec<OsString> = vec![OsString::from("add"), OsString::from(mode)];
arguments.extend(selection_arguments(selection));
let shown: String = arguments
.iter()
.map(|argument| { return argument.to_string_lossy().into_owned(); })
.collect::<Vec<String>>()
.join(" ");
let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
command
.current_dir(cwd)
.env("GIT_INDEX_FILE", commit_index)
.env("GIT_OBJECT_DIRECTORY", object_directory);
let status: std::process::ExitStatus = command
.status_retrying()
.map_err(|error| { return GitFailure(format!("git {shown} could not start: {error}")); })?;
if !status.success() {
return Err(GitFailure(format!(
"git {shown} exited {}",
status.code().map_or_else(|| { return String::from("by signal"); }, |code| { return code.to_string(); })
)));
}
return Ok(());
}
#[cfg(test)]
#[path = "commit_index_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "commit_index_stat_tests.rs"]
mod stat_tests;