use crate::error::{FossilError, Result};
use crate::repo::Repository;
use std::fs;
use std::path::Path;
pub struct GitImportBuilder<'a> {
repo: &'a Repository,
git_url: Option<String>,
branch: Option<String>,
tag: Option<String>,
commit: Option<String>,
message: Option<String>,
author: Option<String>,
include_patterns: Vec<String>,
exclude_patterns: Vec<String>,
target_branch: Option<String>,
parent_commit: Option<String>,
}
impl<'a> GitImportBuilder<'a> {
pub(crate) fn new(repo: &'a Repository) -> Self {
Self {
repo,
git_url: None,
branch: None,
tag: None,
commit: None,
message: None,
author: None,
include_patterns: Vec::new(),
exclude_patterns: Vec::new(),
target_branch: None,
parent_commit: None,
}
}
pub fn url(mut self, url: &str) -> Self {
self.git_url = Some(url.to_string());
self
}
pub fn branch(mut self, branch: &str) -> Self {
self.branch = Some(branch.to_string());
self
}
pub fn tag(mut self, tag: &str) -> Self {
self.tag = Some(tag.to_string());
self
}
pub fn commit_hash(mut self, hash: &str) -> Self {
self.commit = Some(hash.to_string());
self
}
pub fn message(mut self, msg: &str) -> Self {
self.message = Some(msg.to_string());
self
}
pub fn author(mut self, author: &str) -> Self {
self.author = Some(author.to_string());
self
}
pub fn include_pattern(mut self, pattern: &str) -> Self {
self.include_patterns.push(pattern.to_string());
self
}
pub fn exclude_pattern(mut self, pattern: &str) -> Self {
self.exclude_patterns.push(pattern.to_string());
self
}
pub fn to_branch(mut self, branch: &str) -> Self {
self.target_branch = Some(branch.to_string());
self
}
pub fn parent(mut self, hash: &str) -> Self {
self.parent_commit = Some(hash.to_string());
self
}
pub fn execute(self) -> Result<String> {
use herolib_os::git::GitTree;
let git_url = self.git_url.clone().ok_or_else(|| {
FossilError::InvalidArtifact("git URL is required for git import".to_string())
})?;
let message = self.message.clone().ok_or_else(|| {
FossilError::InvalidArtifact("commit message is required for git import".to_string())
})?;
let author = self.author.clone().ok_or_else(|| {
FossilError::InvalidArtifact("author is required for git import".to_string())
})?;
let temp_base =
std::env::temp_dir().join(format!("heroforge_git_import_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_base)?;
let git_tree = GitTree::new(temp_base.to_string_lossy().as_ref()).map_err(|e| {
let _ = fs::remove_dir_all(&temp_base);
FossilError::InvalidArtifact(format!("Failed to create git tree: {}", e))
})?;
let branch_ref = self
.branch
.as_deref()
.or(self.tag.as_deref())
.or(self.commit.as_deref());
let clone_result = if let Some(branch) = branch_ref {
git_tree.clone_repo(&git_url).branch(branch).execute()
} else {
git_tree.clone_repo(&git_url).execute()
};
let cloned_path = clone_result.map_err(|e| {
let _ = fs::remove_dir_all(&temp_base);
FossilError::InvalidArtifact(format!("Failed to clone git repository: {}", e))
})?;
let cloned_dir = Path::new(&cloned_path);
let files = self.collect_files(cloned_dir).map_err(|e| {
let _ = fs::remove_dir_all(&temp_base);
e
})?;
let _ = fs::remove_dir_all(&temp_base);
if files.is_empty() {
return Err(FossilError::InvalidArtifact(
"No files found to import (check include/exclude patterns)".to_string(),
));
}
let parent = if let Some(ref p) = self.parent_commit {
Some(p.clone())
} else {
self.repo
.branch_tip_internal("trunk")
.ok()
.map(|tip| tip.hash)
};
let files_refs: Vec<(&str, &[u8])> = files
.iter()
.map(|(path, content)| (path.as_str(), content.as_slice()))
.collect();
self.repo.commit_internal(
&files_refs,
&message,
&author,
parent.as_deref(),
self.target_branch.as_deref(),
)
}
fn collect_files(&self, dir: &Path) -> Result<Vec<(String, Vec<u8>)>> {
let mut files = Vec::new();
let include_patterns: Vec<glob::Pattern> = self
.include_patterns
.iter()
.filter_map(|p| glob::Pattern::new(p).ok())
.collect();
let exclude_patterns: Vec<glob::Pattern> = self
.exclude_patterns
.iter()
.filter_map(|p| glob::Pattern::new(p).ok())
.collect();
let git_exclude = glob::Pattern::new(".git/**").unwrap();
let git_dir_exclude = glob::Pattern::new(".git").unwrap();
self.collect_files_recursive(
dir,
dir,
&include_patterns,
&exclude_patterns,
&git_exclude,
&git_dir_exclude,
&mut files,
)?;
Ok(files)
}
fn collect_files_recursive(
&self,
base_dir: &Path,
current_dir: &Path,
include_patterns: &[glob::Pattern],
exclude_patterns: &[glob::Pattern],
git_exclude: &glob::Pattern,
git_dir_exclude: &glob::Pattern,
files: &mut Vec<(String, Vec<u8>)>,
) -> Result<()> {
let entries = fs::read_dir(current_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let relative_path = path
.strip_prefix(base_dir)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
if git_exclude.matches(&relative_path) || git_dir_exclude.matches(&relative_path) {
continue;
}
if path.is_dir() {
self.collect_files_recursive(
base_dir,
&path,
include_patterns,
exclude_patterns,
git_exclude,
git_dir_exclude,
files,
)?;
} else if path.is_file() {
let included = if include_patterns.is_empty() {
true
} else {
include_patterns.iter().any(|p| p.matches(&relative_path))
};
let excluded = exclude_patterns.iter().any(|p| p.matches(&relative_path));
if included && !excluded {
let content = fs::read(&path)?;
let normalized_path = relative_path.replace('\\', "/");
files.push((normalized_path, content));
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct GitImportResult {
pub commit_hash: String,
pub files_imported: usize,
pub total_size: usize,
}