use std::path::{Path, PathBuf};
use gix::bstr::ByteSlice;
use walkdir::WalkDir;
use crate::{GitError, GitResult, RepoHandle};
#[derive(Debug, Clone)]
pub struct AddOpts {
pub paths: Vec<PathBuf>,
pub update_only: bool,
pub force: bool,
}
impl AddOpts {
#[inline]
pub fn new<I, P>(paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
Self {
paths: paths.into_iter().map(Into::into).collect(),
update_only: false,
force: false,
}
}
#[inline]
pub fn add_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.paths.push(path.into());
self
}
#[inline]
#[must_use]
pub fn update_only(mut self, yes: bool) -> Self {
self.update_only = yes;
self
}
#[inline]
#[must_use]
pub fn force(mut self, yes: bool) -> Self {
self.force = yes;
self
}
}
#[inline]
fn has_glob_pattern(path: &Path) -> bool {
path.as_os_str()
.as_encoded_bytes()
.iter()
.any(|&b| b == b'*' || b == b'?')
}
#[inline]
fn simple_glob_match(pattern: &[u8], text: &[u8]) -> bool {
simple_glob_match_impl(pattern, text, 0, 0)
}
#[inline]
fn simple_glob_match_impl(
pattern: &[u8],
text: &[u8],
mut pat_idx: usize,
mut text_idx: usize,
) -> bool {
while pat_idx < pattern.len() || text_idx < text.len() {
if pat_idx < pattern.len() {
match pattern[pat_idx] {
b'*' => {
pat_idx += 1;
if pat_idx >= pattern.len() {
return true;
}
while text_idx <= text.len() {
if simple_glob_match_impl(pattern, text, pat_idx, text_idx) {
return true;
}
text_idx += 1;
}
return false;
}
b'?' => {
if text_idx >= text.len() {
return false;
}
pat_idx += 1;
text_idx += 1;
}
byte => {
if text_idx >= text.len() || text[text_idx] != byte {
return false;
}
pat_idx += 1;
text_idx += 1;
}
}
} else {
return false;
}
}
pat_idx >= pattern.len() && text_idx >= text.len()
}
#[inline]
fn expand_paths(paths: &[PathBuf], repo_path: &Path) -> GitResult<Vec<PathBuf>> {
let mut result = Vec::with_capacity(paths.len() * 4);
for input_path in paths {
let full_path = if input_path.is_absolute() {
input_path.clone()
} else {
repo_path.join(input_path)
};
if has_glob_pattern(input_path) {
let parent = full_path.parent().ok_or_else(|| {
GitError::InvalidInput(format!("Invalid glob pattern: {}", input_path.display()))
})?;
let pattern_bytes = full_path
.file_name()
.ok_or_else(|| {
GitError::InvalidInput(format!("Invalid pattern: {}", input_path.display()))
})?
.as_encoded_bytes();
if !parent.exists() {
return Err(GitError::InvalidInput(format!(
"Pattern parent directory does not exist: {}",
parent.display()
)));
}
for entry in WalkDir::new(parent)
.max_depth(1)
.min_depth(1)
.into_iter()
.filter_entry(|e| e.file_type().is_file())
{
let entry = entry.map_err(|e| GitError::Io(e.into()))?;
let filename_bytes = entry.file_name().as_encoded_bytes();
if simple_glob_match(pattern_bytes, filename_bytes) {
result.push(entry.path().to_path_buf());
}
}
} else if full_path.is_dir() {
for entry in WalkDir::new(&full_path).into_iter().filter_entry(|e| {
if e.file_type().is_dir() {
e.file_name() != ".git"
} else {
true
}
}) {
let entry = entry.map_err(|e| GitError::Io(e.into()))?;
if entry.file_type().is_file() {
result.push(entry.path().to_path_buf());
}
}
} else {
result.push(full_path);
}
}
Ok(result)
}
#[inline]
fn process_single_file(
repo: &gix::Repository,
index: &mut gix::index::File,
file_path: &Path,
relative_path: &Path,
symlinks_enabled: bool,
) -> GitResult<()> {
use gix::index::entry::{Flags, Mode, Stat};
let fs_metadata = gix::index::fs::Metadata::from_path_no_follow(file_path)?;
let (blob_data, mode) = if fs_metadata.is_symlink() {
if symlinks_enabled {
let target = std::fs::read_link(file_path)?;
let target_bytes = target.as_os_str().as_encoded_bytes().to_vec();
(target_bytes, Mode::SYMLINK)
} else {
let content = std::fs::read(file_path)?;
#[cfg(unix)]
let is_executable = {
use std::os::unix::fs::PermissionsExt;
let target_metadata = std::fs::metadata(file_path)?;
target_metadata.permissions().mode() & 0o111 != 0
};
#[cfg(not(unix))]
let is_executable = false;
let mode = if is_executable {
Mode::FILE_EXECUTABLE
} else {
Mode::FILE
};
(content, mode)
}
} else {
let content = std::fs::read(file_path)?;
let mode = if fs_metadata.is_executable() {
Mode::FILE_EXECUTABLE
} else {
Mode::FILE
};
(content, mode)
};
let blob_id = repo
.write_blob(&blob_data)
.map_err(|e| GitError::Gix(e.into()))?
.detach();
let stat = Stat::from_fs(&fs_metadata).map_err(|e| {
GitError::InvalidInput(format!(
"Failed to create stat for {}: {}",
file_path.display(),
e
))
})?;
let path_bstr = relative_path.as_os_str().as_encoded_bytes().as_bstr();
index.dangerously_push_entry(stat, blob_id, Flags::empty(), mode, path_bstr);
Ok(())
}
pub async fn add(repo: RepoHandle, opts: AddOpts) -> GitResult<()> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let AddOpts {
paths,
update_only,
force,
} = opts;
if paths.is_empty() {
return Err(GitError::InvalidInput(
"No paths specified for add".to_string(),
));
}
let repo_path = repo_clone.workdir().ok_or_else(|| {
GitError::InvalidInput("Cannot add files in bare repository".to_string())
})?;
let config = repo_clone.config_snapshot();
let symlinks_enabled = config.boolean("core.symlinks").unwrap_or(true);
let expanded_paths = expand_paths(&paths, repo_path)?;
if expanded_paths.is_empty() {
return Err(GitError::InvalidInput(
"No files matched the given patterns".to_string(),
));
}
let mut index = if let Ok(idx) = repo_clone.open_index() {
idx
} else {
let index_path = repo_clone.index_path();
let object_hash = repo_clone.object_hash();
let mut new_index =
gix::index::File::from_state(gix::index::State::new(object_hash), index_path);
new_index
.write(gix::index::write::Options::default())
.map_err(|e| GitError::Gix(e.into()))?;
repo_clone
.open_index()
.map_err(|e| GitError::Gix(e.into()))?
};
let mut excludes = if force {
None
} else {
Some(
repo_clone.excludes(
&index,
None,
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
)
.map_err(|e| GitError::Gix(e.into()))?,
)
};
for file_path in expanded_paths {
let relative_path = file_path
.strip_prefix(repo_path)
.map_err(|_| {
GitError::InvalidInput(format!(
"Path {} is not within repository",
file_path.display()
))
})?
.to_path_buf();
let path_bstr = relative_path.as_os_str().as_encoded_bytes().as_bstr();
if update_only && index.entry_by_path(path_bstr).is_none() {
continue;
}
if let Some(ref mut exc) = excludes {
let platform = exc.at_entry(path_bstr, None)?;
if platform.is_excluded() {
continue;
}
}
process_single_file(
&repo_clone,
&mut index,
&file_path,
&relative_path,
symlinks_enabled,
)?;
}
index.sort_entries();
use gix::index::write::Options;
index
.write(Options::default())
.map_err(|e| GitError::Gix(e.into()))?;
Ok(())
})
.await
.map_err(|e| GitError::InvalidInput(format!("Task join error: {e}")))?
}