use crate::capture_invocation::InvocationCapture;
use crate::private_index_request::{CaptureIndexes, indexed_request};
use crate::commit_conclusion::events_with_summary;
use crate::commit_no_change::{NO_CHANGE_CODE, NO_CHANGE_CORE_ID, no_change_message};
use crate::commit_transaction::{
CommitInvocation, TransactionResult, TransactionStop, failed,
};
use crate::config_snapshot::ConfigSnapshot;
use crate::policy_events::PolicyEvent;
use crate::private_storage::write_private_file;
use crate::service_land_follow::{IndexPaths, update_index};
use crate::service_protocol::{ALLOWED_MODES, Change, MAX_CHANGES, is_object_name, is_plain_path};
use crate::transaction_git::{GitContext, run_git_checked};
use crate::transaction_journal::AddedPath;
use crate::worktree_completion::install_added_worktree_files;
use std::path::{Path, PathBuf};
use crate::service_prepare_ready_shapes::Prepared;
pub fn zero_object(empty_tree: &str) -> String {
return "0".repeat(empty_tree.len());
}
pub fn changes_of(context: &GitContext, cwd: &Path, indexes: &CaptureIndexes, base: &str) -> Result<Option<Vec<Change>>, TransactionStop> {
let arguments: Vec<String> = ["diff-index", "--cached", "-r", "--raw", "-z", "--no-renames", "--no-abbrev", base].map(String::from).to_vec();
let output = run_git_checked(
context,
&indexed_request(cwd, &arguments, Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())),
)?;
let text: String = match String::from_utf8(output.stdout) {
Ok(text) => text,
Err(_) => return Ok(None),
};
let mut fields = text.split('\0').filter(is_not_empty_field);
let mut changes: Vec<Change> = Vec::new();
while let Some(header) = fields.next() {
let Some(path) = fields.next() else { return Ok(None) };
let parts: Vec<&str> = header.trim_start_matches(':').split(' ').collect();
let [_, new_mode, _, new_name, status] = parts.as_slice() else { return Ok(None) };
let deletion: bool = status.starts_with('D');
let mode: u32 = if deletion { ALLOWED_MODES[0] } else { u32::from_str_radix(new_mode, 8).unwrap_or(0) };
if !is_plain_path(path) || !ALLOWED_MODES.contains(&mode) || (!deletion && !is_object_name(new_name)) {
return Ok(None);
}
changes.push(Change { path: path.to_string(), mode, blob: if deletion { None } else { Some((*new_name).to_string()) } });
if changes.len() > MAX_CHANGES {
return Ok(None);
}
}
return Ok(Some(changes));
}
pub(crate) fn is_not_empty_field(field: &&str) -> bool {
return !field.is_empty();
}
pub fn config_needs_native(snapshot: Option<&ConfigSnapshot>) -> bool {
const KEYS: [&str; 7] = ["commit.template", "commit.cleanup", "i18n.commitencoding", "core.commentchar", "core.commentstring", "commit.signoff", "commit.verbose"];
let Some(found) = snapshot else { return true };
for entry in found.entries().iter() {
let value: String = match entry.value.as_deref() {
Some(bytes) => String::from_utf8_lossy(bytes).to_lowercase(),
None => String::new(),
};
let signs: bool = entry.key == "commit.gpgsign" && !["false", "no", "off", "0"].contains(&value.as_str());
if signs || KEYS.contains(&entry.key.as_str()) || entry.key.starts_with("trailer.") {
return true;
}
}
return false;
}
pub(crate) fn pathspec_file_for(scratch: &Path, cwd: &Path, source: Option<&str>, standard_input: Option<&[u8]>) -> std::io::Result<Option<PathBuf>> {
let Some(name) = source else { return Ok(None) };
if name != "-" {
let path: PathBuf = PathBuf::from(name);
return Ok(Some(if path.is_absolute() { path } else { cwd.join(path) }));
}
let snapshot: PathBuf = scratch.join(crate::commit_index::PATHSPEC_INPUT_NAME);
write_private_file(snapshot.as_path(), standard_input.unwrap_or_default())?;
return Ok(Some(snapshot));
}
pub(crate) fn normalized(
invocation: &CommitInvocation<'_>,
capture: &InvocationCapture,
base: &str,
committed: &[Vec<u8>],
records: &[AddedPath],
settled: &(Vec<PolicyEvent>, u64, Vec<Vec<u8>>),
) -> Result<Prepared, TransactionStop> {
let (events, passes, changed_paths) = settled;
if let Err(code) = update_index(invocation.context, capture.layout.repository_root.as_path(), capture.layout.real_index_path.as_path(), base, IndexPaths::Named(committed)) {
return Err(failed(format!("the index could not be reconciled with the corrected files: {}", code.word())));
}
let completion = match install_added_worktree_files(
invocation.context,
invocation.cwd,
capture.layout.repository_root.as_path(),
records,
Some(capture.layout.object_directory.as_path()),
) {
Ok(done) => done,
Err(error) => return Err(TransactionStop::Message(error.0)),
};
let mut summary: Vec<PolicyEvent> = events_with_summary(events.clone(), *passes, changed_paths.as_slice());
summary.push(PolicyEvent::CoreFinding {
core_id: NO_CHANGE_CORE_ID,
code: NO_CHANGE_CODE,
message: no_change_message(completion.conflicted.as_slice()),
});
return Ok(Prepared::Ended(TransactionResult { events: summary, exit_code: 1, landed_oid: None, uses_lfs: false }));
}