use super::config_snapshot::{ConfigEntry, read_config_snapshot};
use super::landed_summary::short_name;
use super::repo_read_layout::ObjectFormat;
use super::repo_read_object::from_hex;
use super::repo_read_odb::ObjectDatabase;
use super::repo_read_tree::{CommitFields, FlatEntry, flatten_tree, parse_commit, subject_of};
use super::transaction_git::GitContext;
use super::worktree_add_plan::AddPlan;
use super::worktree_clean::head_commit;
use super::worktree_copy::{GitRunner, GitStatus, GitStep};
use super::fs_beneath::handle_path;
use super::worktree_add_binding::{Bound, path_identity};
use super::worktree_snapshot_source::SnapshotSource;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Target {
pub admin: PathBuf,
pub pointer: Vec<u8>,
pub commit: String,
pub differs: bool,
pub line: String,
}
fn attribute_files(tree: &[FlatEntry]) -> Vec<(&[u8], &[u8])> {
return tree
.iter()
.filter(|entry: &&FlatEntry| { return entry.path == b".gitattributes" || entry.path.ends_with(b"/.gitattributes"); })
.map(|entry: &FlatEntry| { return (entry.path.as_slice(), entry.id.as_slice()); })
.collect();
}
fn attributes_agree(database: &ObjectDatabase, from: &[u8], to: &[u8], format: ObjectFormat) -> Result<(), String> {
let (Some(old), Some(new)) = (flatten_tree(database, from, format), flatten_tree(database, to, format)) else { return Err(String::from("a tree is unreadable")) };
return if attribute_files(old.as_slice()) == attribute_files(new.as_slice()) { Ok(()) } else { Err(String::from("the .gitattributes files differ between the source and the new HEAD")) };
}
const CONVERSION_KEYS: &[&str] = &[
"core.autocrlf",
"core.eol",
"core.safecrlf",
"core.attributesfile",
"core.symlinks",
"core.filemode",
"core.ignorecase",
"core.precomposeunicode",
"core.protectntfs",
"core.protecthfs",
"core.checkroundtripencoding",
"core.sparsecheckout",
"core.sparsecheckoutcone",
];
fn conversion_settings(entries: &[ConfigEntry]) -> Vec<(String, Option<Vec<u8>>)> {
return entries
.iter()
.filter(|entry: &&ConfigEntry| { return entry.key.starts_with("filter.") || entry.key.starts_with("extensions.") || CONVERSION_KEYS.contains(&entry.key.as_str()); })
.map(|entry: &ConfigEntry| { return (entry.key.clone(), entry.value.clone()); })
.collect();
}
fn conversion_agrees(source: &[ConfigEntry], target: &[ConfigEntry]) -> Result<(), String> {
let (old, new): (Vec<(String, Option<Vec<u8>>)>, Vec<(String, Option<Vec<u8>>)>) = (conversion_settings(source), conversion_settings(target));
if old == new {
return Ok(());
}
let first: usize = (0..old.len().max(new.len())).find(|index: &usize| { return old.get(*index) != new.get(*index); }).unwrap_or(0);
let key: String = old.get(first).or_else(|| { return new.get(first); }).map(|(name, _value): &(String, Option<Vec<u8>>)| { return name.clone(); }).unwrap_or_default();
return Err(format!("the conversion setting {key} differs between the source and the new worktree, or comes in another order"));
}
fn filters_named(text: &str) -> Vec<String> {
return text
.lines()
.filter(|line: &&str| { return !line.trim_start().starts_with('#'); })
.flat_map(|line: &str| { return line.split_whitespace().filter_map(|token: &str| { return token.strip_prefix("filter=").map(str::to_owned); }); })
.collect();
}
const LFS_FILTER: [(&str, &str); 4] = [
("filter.lfs.clean", "git-lfs clean -- %f"),
("filter.lfs.smudge", "git-lfs smudge -- %f"),
("filter.lfs.process", "git-lfs filter-process"),
("filter.lfs.required", "true"),
];
fn external_conversion_refusal(entries: &[ConfigEntry], attribute_texts: &[String]) -> Option<String> {
if let Some(entry) = entries.iter().find(|entry: &&ConfigEntry| { return entry.key == "core.attributesfile" && !entry.value.as_deref().is_some_and(|value: &[u8]| { return value.starts_with(b"/"); }); }) {
return Some(format!("core.attributesFile is {:?}, which is not an absolute path", entry.value.as_deref().map(String::from_utf8_lossy)));
}
let used: Vec<String> = attribute_texts.iter().flat_map(|text: &String| { return filters_named(text.as_str()); }).collect();
for name in &used {
if name != "lfs" {
return Some(format!("an attribute applies the filter {name}, whose result is not proven equal in both worktrees"));
}
let value = |key: &str| -> Option<String> { return entries.iter().rev().find(|entry: &&ConfigEntry| { return entry.key == key; }).and_then(|entry: &ConfigEntry| { return entry.value.as_deref().map(|bytes: &[u8]| { return String::from_utf8_lossy(bytes).into_owned(); }); }); };
if LFS_FILTER.iter().any(|(key, expected): &(&str, &str)| { return value(key).as_deref() != Some(*expected); }) {
return Some(String::from("the lfs filter is not the standard Git LFS definition"));
}
if entries.iter().any(|entry: &ConfigEntry| { return entry.key.starts_with("lfs.extension."); }) {
return Some(String::from("Git LFS extensions are configured"));
}
}
return None;
}
fn untracked_attribute_texts(source: &SnapshotSource, entries: &[ConfigEntry]) -> Vec<String> {
let mut paths: Vec<PathBuf> = vec![source.common_dir.join("info/attributes")];
if let Some(value) = entries.iter().rev().find(|entry: &&ConfigEntry| { return entry.key == "core.attributesfile"; }).and_then(|entry: &ConfigEntry| { return entry.value.as_deref(); }) {
paths.push(PathBuf::from(String::from_utf8_lossy(value).into_owned()));
}
if let Some(home) = std::env::var_os("HOME") {
paths.push(PathBuf::from(home).join(".config/git/attributes"));
}
if let Some(config) = std::env::var_os("XDG_CONFIG_HOME") {
paths.push(PathBuf::from(config).join("git/attributes"));
}
return paths.iter().filter_map(|path: &PathBuf| { return std::fs::read(path.as_path()).ok(); }).map(|bytes: Vec<u8>| { return String::from_utf8_lossy(bytes.as_slice()).into_owned(); }).collect();
}
pub(super) fn run_step(run_git: &mut GitRunner, token: Option<&str>, arguments: &[OsString], environment: &[(OsString, OsString)]) -> Result<GitStatus, String> {
return run_git(token, &GitStep::Other { arguments, environment }).map_err(|error| { return error.0; });
}
pub(super) fn checkout_environment(git_dir: &Path, work_tree: &Path) -> Vec<(OsString, OsString)> {
return vec![(OsString::from("GIT_DIR"), git_dir.as_os_str().to_os_string()), (OsString::from("GIT_WORK_TREE"), work_tree.as_os_str().to_os_string())];
}
pub fn prepare_target(source: &SnapshotSource, plan: &AddPlan, context: &GitContext, bound: &Bound) -> Result<Target, String> {
let destination: &Path = plan.destination.as_path();
if bound.binding.destination != destination {
return Err(String::from("the add is bound to another destination"));
}
let pointer: Vec<u8> = bound.binding.pointer.clone();
let admin: PathBuf = bound.binding.admin.clone();
if path_identity(destination) != Some(bound.binding.destination_id) {
return Err(String::from("the destination is no longer the directory Git made"));
}
let held_entries: usize = std::fs::read_dir(handle_path(&bound.destination)).map_err(|error| { return format!("the destination is unreadable ({error})"); })?.count();
if held_entries != 1 {
return Err(String::from("the destination holds more than Git's .git file"));
}
let target_config = read_config_snapshot(context, destination).ok_or_else(|| { return String::from("the new worktree's configuration could not be read"); })?;
conversion_agrees(source.config.entries(), target_config.entries())?;
let format: ObjectFormat = if source.clean.hash_length == 32 { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
let commit: String = head_commit(admin.as_path(), source.common_dir.as_path(), format).ok_or_else(|| { return String::from("the new HEAD is not a plain ref to a commit"); })?;
let database: ObjectDatabase = ObjectDatabase::open(source.common_dir.as_path(), None, format).ok_or_else(|| { return String::from("the object database is not plain"); })?;
let name: Vec<u8> = from_hex(commit.as_bytes(), format).ok_or_else(|| { return String::from("the new HEAD is not an object name"); })?;
let raw = database.read(name.as_slice()).ok_or_else(|| { return String::from("the new HEAD commit is unreadable"); })?;
let fields: CommitFields = parse_commit(raw.bytes.as_slice(), format).ok_or_else(|| { return String::from("the new HEAD commit has a layout the reader does not know"); })?;
let differs: bool = fields.tree != source.clean.head_tree;
if differs {
attributes_agree(&database, source.clean.head_tree.as_slice(), fields.tree.as_slice(), format)?;
}
let mut attribute_texts: Vec<String> = untracked_attribute_texts(source, target_config.entries());
for (path, id) in &source.clean.attribute_blobs {
let blob = database.read(id.as_slice()).ok_or_else(|| { return format!("{} is unreadable", String::from_utf8_lossy(path)); })?;
attribute_texts.push(String::from_utf8_lossy(blob.bytes.as_slice()).into_owned());
}
if let Some(reason) = external_conversion_refusal(target_config.entries(), attribute_texts.as_slice()) {
return Err(reason);
}
let line: String = if plan.quiet {
String::new()
} else {
let subject: Vec<u8> = subject_of(fields.message.as_slice()).ok_or_else(|| { return String::from("the subject of the new HEAD needs Git to print"); })?;
format!("HEAD is now at {} {}\n", short_name(context, destination, source.common_dir.join("objects").as_path(), Some(&source.config), commit.as_str()), String::from_utf8_lossy(subject.as_slice()))
};
return Ok(Target { admin, pointer, commit, differs, line });
}
pub fn apply_difference(source: &SnapshotSource, target: &Target, work_tree: &Path, token: Option<&str>, run_git: &mut GitRunner) -> Result<(), String> {
if !target.differs {
return Ok(());
}
let arguments: Vec<OsString> = vec![
OsString::from("-C"),
work_tree.as_os_str().to_os_string(),
OsString::from("read-tree"),
OsString::from("-m"),
OsString::from("-u"),
OsString::from(source.clean.head_commit.as_str()),
OsString::from(target.commit.as_str()),
];
let status: GitStatus = run_step(run_git, token, arguments.as_slice(), checkout_environment(target.admin.as_path(), work_tree).as_slice())?;
return if status.succeeded() { Ok(()) } else { Err(format!("`git read-tree -m -u` ended with {:?}", status.code)) };
}
#[cfg(test)]
#[path = "worktree_ladder_target_tests.rs"]
mod tests;