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;
pub use crate::commit_index_copying::copy_index_with_timestamps;
use crate::commit_index_copying::{copy_index_failure, seeds_private_index};
use crate::forwarding::git_command;
use std::ffi::{OsStr, OsString};
use std::path::Path;
pub const PATHSPEC_INPUT_NAME: &str = "pathspec.input";
#[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)),
);
}
fn is_nul_byte(byte: &u8) -> bool {
return *byte == 0;
}
fn is_tab_byte(byte: &u8) -> bool {
return *byte == b'\t';
}
pub fn initialize_commit_index(
context: &GitContext,
cwd: &Path,
real_index: &Path,
indexes: &CaptureIndexes,
selection: &IndexSelection,
) -> Result<(), GitFailure> {
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_index_failure)?;
if selection.mode == TransactionMode::Index {
copy_index_with_timestamps(indexes.captured_index.as_path(), indexes.commit_index.as_path())
.map_err(copy_index_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_index_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 match crate::repo_read_hook::try_request(context, &request) {
Some(answer) => answer.code == Some(0) && answer.stdout.is_empty(),
None => false,
};
}
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 = match std::str::from_utf8(output.stdout.as_slice()) {
Ok(decoded) => decoded,
Err(_) => return Err(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(is_nul_byte) {
let Some(tab) = record.iter().position(is_tab_byte) 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),
),
)?;
let mut paths: Vec<Vec<u8>> = Vec::new();
for path in output.stdout.split(is_nul_byte) {
if !path.is_empty() {
paths.push(path.to_vec());
}
}
return Ok(paths);
}
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 mut shown_words: Vec<String> = Vec::new();
for argument in &arguments {
shown_words.push(argument.to_string_lossy().into_owned());
}
let shown: String = shown_words.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 = match command.status_retrying() {
Ok(finished) => finished,
Err(error) => return Err(GitFailure(format!("git {shown} could not start: {error}"))),
};
if !status.success() {
return Err(GitFailure(format!(
"git {shown} exited {}",
match status.code() {
Some(code) => code.to_string(),
None => String::from("by signal"),
}
)));
}
return Ok(());
}
#[cfg(test)]
#[path = "commit_index_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "commit_index_stat_tests.rs"]
mod stat_tests;