use crate::error::{FossilError, Result};
use crate::fs::find::Permissions;
use crate::repo::Repository;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Op {
Copy {
src: String,
dst: String,
},
Move {
src: String,
dst: String,
},
Delete {
path: String,
recursive: bool,
},
Chmod {
path: String,
permissions: Permissions,
recursive: bool,
},
Symlink {
link_path: String,
target: String,
},
Write {
path: String,
content: Vec<u8>,
},
MakeExecutable {
path: String,
},
}
#[derive(Debug)]
pub struct Preview {
pub base_commit: String,
pub base_file_count: usize,
pub operations: Vec<Op>,
}
impl Preview {
pub fn describe(&self) -> Vec<String> {
self.operations
.iter()
.map(|op| match op {
Op::Copy { src, dst } => format!("COPY {} -> {}", src, dst),
Op::Move { src, dst } => format!("MOVE {} -> {}", src, dst),
Op::Delete { path, recursive } => {
if *recursive {
format!("DELETE {} (recursive)", path)
} else if path.starts_with("glob:") {
format!("DELETE matching {}", &path[5..])
} else {
format!("DELETE {}", path)
}
}
Op::Chmod {
path,
permissions,
recursive,
} => {
if *recursive {
format!("CHMOD {} {:o} (recursive)", path, permissions.to_octal())
} else {
format!("CHMOD {} {:o}", path, permissions.to_octal())
}
}
Op::Symlink { link_path, target } => {
format!("SYMLINK {} -> {}", link_path, target)
}
Op::Write { path, content } => {
format!("WRITE {} ({} bytes)", path, content.len())
}
Op::MakeExecutable { path } => format!("MAKE_EXECUTABLE {}", path),
})
.collect()
}
}
pub struct Modify<'a> {
repo: &'a Repository,
base_commit: Option<String>,
operations: Vec<Op>,
commit_message: Option<String>,
author: Option<String>,
branch: Option<String>,
}
impl<'a> Modify<'a> {
pub fn new(repo: &'a Repository) -> Self {
Self {
repo,
base_commit: None,
operations: Vec::new(),
commit_message: None,
author: None,
branch: None,
}
}
pub fn at_commit(mut self, hash: &str) -> Self {
self.base_commit = Some(hash.to_string());
self
}
pub fn on_trunk(self) -> Self {
self
}
pub fn on_branch(mut self, branch: &str) -> Result<Self> {
let tip = self.repo.branch_tip_internal(branch)?;
self.base_commit = Some(tip.hash);
self.branch = Some(branch.to_string());
Ok(self)
}
pub fn message(mut self, msg: &str) -> Self {
self.commit_message = Some(msg.to_string());
self
}
pub fn author(mut self, author: &str) -> Self {
self.author = Some(author.to_string());
self
}
pub fn copy_file(mut self, src: &str, dst: &str) -> Self {
self.operations.push(Op::Copy {
src: src.to_string(),
dst: dst.to_string(),
});
self
}
pub fn copy_dir(mut self, src: &str, dst: &str) -> Self {
self.operations.push(Op::Copy {
src: format!("{}/", src.trim_end_matches('/')),
dst: format!("{}/", dst.trim_end_matches('/')),
});
self
}
pub fn move_file(mut self, src: &str, dst: &str) -> Self {
self.operations.push(Op::Move {
src: src.to_string(),
dst: dst.to_string(),
});
self
}
pub fn move_dir(mut self, src: &str, dst: &str) -> Self {
self.operations.push(Op::Move {
src: format!("{}/", src.trim_end_matches('/')),
dst: format!("{}/", dst.trim_end_matches('/')),
});
self
}
pub fn rename(self, old_path: &str, new_path: &str) -> Self {
self.move_file(old_path, new_path)
}
pub fn delete_file(mut self, path: &str) -> Self {
self.operations.push(Op::Delete {
path: path.to_string(),
recursive: false,
});
self
}
pub fn delete_dir(mut self, path: &str) -> Self {
self.operations.push(Op::Delete {
path: format!("{}/", path.trim_end_matches('/')),
recursive: true,
});
self
}
pub fn delete_matching(mut self, pattern: &str) -> Self {
self.operations.push(Op::Delete {
path: format!("glob:{}", pattern),
recursive: false,
});
self
}
pub fn chmod(mut self, path: &str, mode: u32) -> Self {
self.operations.push(Op::Chmod {
path: path.to_string(),
permissions: Permissions::from_octal(mode),
recursive: false,
});
self
}
pub fn chmod_permissions(mut self, path: &str, perms: Permissions) -> Self {
self.operations.push(Op::Chmod {
path: path.to_string(),
permissions: perms,
recursive: false,
});
self
}
pub fn chmod_dir(mut self, path: &str, mode: u32) -> Self {
self.operations.push(Op::Chmod {
path: format!("{}/", path.trim_end_matches('/')),
permissions: Permissions::from_octal(mode),
recursive: true,
});
self
}
pub fn make_executable(mut self, path: &str) -> Self {
self.operations.push(Op::MakeExecutable {
path: path.to_string(),
});
self
}
pub fn symlink(mut self, link_path: &str, target: &str) -> Self {
self.operations.push(Op::Symlink {
link_path: link_path.to_string(),
target: target.to_string(),
});
self
}
pub fn symlink_file(self, link_path: &str, target_file: &str) -> Self {
self.symlink(link_path, target_file)
}
pub fn symlink_dir(self, link_path: &str, target_dir: &str) -> Self {
self.symlink(link_path, target_dir)
}
pub fn write(mut self, path: &str, content: &[u8]) -> Self {
self.operations.push(Op::Write {
path: path.to_string(),
content: content.to_vec(),
});
self
}
pub fn write_str(self, path: &str, content: &str) -> Self {
self.write(path, content.as_bytes())
}
pub fn touch(self, path: &str) -> Self {
self.write(path, &[])
}
pub fn execute(self) -> Result<String> {
let message = self.commit_message.ok_or_else(|| {
FossilError::InvalidArtifact("commit message required for fs operations".to_string())
})?;
let author = self.author.ok_or_else(|| {
FossilError::InvalidArtifact("author required for fs operations".to_string())
})?;
let base_hash = if let Some(hash) = self.base_commit {
hash
} else {
let tip = self.repo.branch_tip_internal("trunk")?;
tip.hash
};
let base_files = self.repo.list_files_internal(&base_hash)?;
let mut file_contents: HashMap<String, Vec<u8>> = HashMap::new();
let mut file_permissions: HashMap<String, String> = HashMap::new();
let mut _symlinks: HashMap<String, String> = HashMap::new();
for file in &base_files {
let content = self.repo.read_file_internal(&base_hash, &file.name)?;
file_contents.insert(file.name.clone(), content);
if let Some(ref perms) = file.permissions {
file_permissions.insert(file.name.clone(), perms.clone());
}
}
for op in &self.operations {
match op {
Op::Copy { src, dst } => {
if src.ends_with('/') {
let src_prefix = src.trim_end_matches('/');
let dst_prefix = dst.trim_end_matches('/');
let to_copy: Vec<_> = file_contents
.keys()
.filter(|k| k.starts_with(src_prefix))
.cloned()
.collect();
for path in to_copy {
let new_path = path.replacen(src_prefix, dst_prefix, 1);
if let Some(content) = file_contents.get(&path).cloned() {
file_contents.insert(new_path.clone(), content);
}
if let Some(perms) = file_permissions.get(&path).cloned() {
file_permissions.insert(new_path, perms);
}
}
} else {
if let Some(content) = file_contents.get(src).cloned() {
file_contents.insert(dst.clone(), content);
}
if let Some(perms) = file_permissions.get(src).cloned() {
file_permissions.insert(dst.clone(), perms);
}
}
}
Op::Move { src, dst } => {
if src.ends_with('/') {
let src_prefix = src.trim_end_matches('/');
let dst_prefix = dst.trim_end_matches('/');
let to_move: Vec<_> = file_contents
.keys()
.filter(|k| k.starts_with(src_prefix))
.cloned()
.collect();
for path in to_move {
let new_path = path.replacen(src_prefix, dst_prefix, 1);
if let Some(content) = file_contents.remove(&path) {
file_contents.insert(new_path.clone(), content);
}
if let Some(perms) = file_permissions.remove(&path) {
file_permissions.insert(new_path, perms);
}
}
} else {
if let Some(content) = file_contents.remove(src) {
file_contents.insert(dst.clone(), content);
}
if let Some(perms) = file_permissions.remove(src) {
file_permissions.insert(dst.clone(), perms);
}
}
}
Op::Delete { path, recursive } => {
if path.starts_with("glob:") {
let pattern = &path[5..];
if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
let to_delete: Vec<_> = file_contents
.keys()
.filter(|k| glob_pattern.matches(k))
.cloned()
.collect();
for p in to_delete {
file_contents.remove(&p);
file_permissions.remove(&p);
}
}
} else if *recursive || path.ends_with('/') {
let prefix = path.trim_end_matches('/');
let to_delete: Vec<_> = file_contents
.keys()
.filter(|k| k.starts_with(prefix) || *k == prefix)
.cloned()
.collect();
for p in to_delete {
file_contents.remove(&p);
file_permissions.remove(&p);
}
} else {
file_contents.remove(path);
file_permissions.remove(path);
}
}
Op::Chmod {
path,
permissions,
recursive,
} => {
let perm_str = format!("{:o}", permissions.to_octal());
if *recursive || path.ends_with('/') {
let prefix = path.trim_end_matches('/');
let to_chmod: Vec<_> = file_contents
.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect();
for p in to_chmod {
file_permissions.insert(p, perm_str.clone());
}
} else if file_contents.contains_key(path) {
file_permissions.insert(path.clone(), perm_str);
}
}
Op::MakeExecutable { path } => {
if file_contents.contains_key(path) {
file_permissions.insert(path.clone(), "755".to_string());
}
}
Op::Symlink { link_path, target } => {
let symlink_content = format!("link {}", target);
file_contents.insert(link_path.clone(), symlink_content.into_bytes());
_symlinks.insert(link_path.clone(), target.clone());
}
Op::Write { path, content } => {
file_contents.insert(path.clone(), content.clone());
}
}
}
let files: Vec<(&str, &[u8])> = file_contents
.iter()
.map(|(k, v)| (k.as_str(), v.as_slice()))
.collect();
self.repo.commit_internal(
&files,
&message,
&author,
Some(&base_hash),
self.branch.as_deref(),
)
}
pub fn preview(&self) -> Result<Preview> {
let base_hash = if let Some(ref hash) = self.base_commit {
hash.clone()
} else {
let tip = self.repo.branch_tip_internal("trunk")?;
tip.hash
};
let base_files = self.repo.list_files_internal(&base_hash)?;
Ok(Preview {
base_commit: base_hash,
base_file_count: base_files.len(),
operations: self.operations.clone(),
})
}
}
use std::path::Path;
pub fn upload<P: AsRef<Path>>(
repo: &Repository,
os_path: P,
repo_path: &str,
author: &str,
message: Option<&str>,
) -> Result<String> {
let os_path = os_path.as_ref();
let content = std::fs::read(os_path).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to read file '{}': {}", os_path.display(), e),
))
})?;
let msg = message.unwrap_or_else(|| "Upload file");
let full_message = format!("{}: {}", msg, repo_path);
Modify::new(repo)
.message(&full_message)
.author(author)
.write(repo_path, &content)
.execute()
}
pub fn upload_dir<P: AsRef<Path>>(
repo: &Repository,
os_path: P,
repo_path: &str,
author: &str,
message: Option<&str>,
) -> Result<String> {
let os_path = os_path.as_ref();
if !os_path.is_dir() {
return Err(FossilError::Io(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
format!("'{}' is not a directory", os_path.display()),
)));
}
let mut modify = Modify::new(repo);
let msg = message.unwrap_or("Upload directory");
modify = modify
.message(&format!("{}: {}", msg, repo_path))
.author(author);
fn collect_files<'a, P: AsRef<Path>>(
dir: P,
base: &Path,
repo_base: &str,
modify: Modify<'a>,
) -> Result<Modify<'a>> {
let mut m = modify;
let entries = std::fs::read_dir(dir.as_ref()).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!(
"Failed to read directory '{}': {}",
dir.as_ref().display(),
e
),
))
})?;
for entry in entries {
let entry = entry.map_err(|e| FossilError::Io(e))?;
let path = entry.path();
if path.is_file() {
let relative = path.strip_prefix(base).unwrap_or(&path);
let repo_file_path = if repo_base.is_empty() {
relative.to_string_lossy().to_string()
} else {
format!(
"{}/{}",
repo_base.trim_end_matches('/'),
relative.to_string_lossy()
)
};
let content = std::fs::read(&path).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to read file '{}': {}", path.display(), e),
))
})?;
m = m.write(&repo_file_path, &content);
} else if path.is_dir() {
m = collect_files(&path, base, repo_base, m)?;
}
}
Ok(m)
}
let modify = collect_files(os_path, os_path, repo_path, modify)?;
modify.execute()
}
pub fn download<P: AsRef<Path>>(repo: &Repository, repo_path: &str, os_path: P) -> Result<()> {
let os_path = os_path.as_ref();
let tip = repo.branch_tip_internal("trunk")?;
let content = repo.read_file_internal(&tip.hash, repo_path)?;
if let Some(parent) = os_path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to create directory '{}': {}", parent.display(), e),
))
})?;
}
}
std::fs::write(os_path, &content).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to write file '{}': {}", os_path.display(), e),
))
})?;
Ok(())
}
pub fn download_from_branch<P: AsRef<Path>>(
repo: &Repository,
branch: &str,
repo_path: &str,
os_path: P,
) -> Result<()> {
let os_path = os_path.as_ref();
let tip = repo.branch_tip_internal(branch)?;
let content = repo.read_file_internal(&tip.hash, repo_path)?;
if let Some(parent) = os_path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to create directory '{}': {}", parent.display(), e),
))
})?;
}
}
std::fs::write(os_path, &content).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to write file '{}': {}", os_path.display(), e),
))
})?;
Ok(())
}
pub fn download_dir<P: AsRef<Path>>(
repo: &Repository,
repo_path: &str,
os_path: P,
) -> Result<usize> {
let os_path = os_path.as_ref();
let tip = repo.branch_tip_internal("trunk")?;
let all_files = repo.list_files_internal(&tip.hash)?;
let prefix = if repo_path.is_empty() {
String::new()
} else {
format!("{}/", repo_path.trim_end_matches('/'))
};
let mut count = 0;
for file in all_files {
let relative_path = if prefix.is_empty() {
Some(file.name.as_str())
} else if file.name.starts_with(&prefix) {
Some(&file.name[prefix.len()..])
} else {
None
};
if let Some(rel_path) = relative_path {
let dest_path = os_path.join(rel_path);
if let Some(parent) = dest_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to create directory '{}': {}", parent.display(), e),
))
})?;
}
}
let content = repo.read_file_internal(&tip.hash, &file.name)?;
std::fs::write(&dest_path, &content).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to write file '{}': {}", dest_path.display(), e),
))
})?;
count += 1;
}
}
Ok(count)
}
pub fn download_matching<P: AsRef<Path>>(
repo: &Repository,
pattern: &str,
os_path: P,
) -> Result<usize> {
let os_path = os_path.as_ref();
let tip = repo.branch_tip_internal("trunk")?;
let matched_files = repo.find_files_internal(&tip.hash, pattern)?;
let mut count = 0;
for file in matched_files {
let dest_path = os_path.join(&file.name);
if let Some(parent) = dest_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to create directory '{}': {}", parent.display(), e),
))
})?;
}
}
let content = repo.read_file_internal(&tip.hash, &file.name)?;
std::fs::write(&dest_path, &content).map_err(|e| {
FossilError::Io(std::io::Error::new(
e.kind(),
format!("Failed to write file '{}': {}", dest_path.display(), e),
))
})?;
count += 1;
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preview_describe() {
let preview = Preview {
base_commit: "abc123".to_string(),
base_file_count: 10,
operations: vec![
Op::Copy {
src: "a.txt".to_string(),
dst: "b.txt".to_string(),
},
Op::Move {
src: "old/".to_string(),
dst: "new/".to_string(),
},
Op::Delete {
path: "temp.log".to_string(),
recursive: false,
},
Op::Delete {
path: "cache/".to_string(),
recursive: true,
},
Op::Chmod {
path: "script.sh".to_string(),
permissions: Permissions::executable(),
recursive: false,
},
Op::Symlink {
link_path: "link".to_string(),
target: "target".to_string(),
},
Op::Write {
path: "file.txt".to_string(),
content: b"hello".to_vec(),
},
Op::MakeExecutable {
path: "run.sh".to_string(),
},
],
};
let descriptions = preview.describe();
assert_eq!(descriptions.len(), 8);
assert_eq!(descriptions[0], "COPY a.txt -> b.txt");
assert_eq!(descriptions[1], "MOVE old/ -> new/");
assert_eq!(descriptions[2], "DELETE temp.log");
assert_eq!(descriptions[3], "DELETE cache/ (recursive)");
assert_eq!(descriptions[4], "CHMOD script.sh 755");
assert_eq!(descriptions[5], "SYMLINK link -> target");
assert_eq!(descriptions[6], "WRITE file.txt (5 bytes)");
assert_eq!(descriptions[7], "MAKE_EXECUTABLE run.sh");
}
#[test]
fn test_preview_describe_glob_delete() {
let preview = Preview {
base_commit: "hash".to_string(),
base_file_count: 5,
operations: vec![Op::Delete {
path: "glob:**/*.bak".to_string(),
recursive: false,
}],
};
let descriptions = preview.describe();
assert_eq!(descriptions[0], "DELETE matching **/*.bak");
}
#[test]
fn test_preview_describe_recursive_chmod() {
let preview = Preview {
base_commit: "hash".to_string(),
base_file_count: 5,
operations: vec![Op::Chmod {
path: "bin/".to_string(),
permissions: Permissions::from_octal(0o755),
recursive: true,
}],
};
let descriptions = preview.describe();
assert_eq!(descriptions[0], "CHMOD bin/ 755 (recursive)");
}
#[test]
fn test_preview_empty_operations() {
let preview = Preview {
base_commit: "abc123".to_string(),
base_file_count: 0,
operations: vec![],
};
assert!(preview.describe().is_empty());
assert_eq!(preview.base_file_count, 0);
}
#[test]
fn test_op_copy_clone() {
let op = Op::Copy {
src: "src.txt".to_string(),
dst: "dst.txt".to_string(),
};
let cloned = op.clone();
if let Op::Copy { src, dst } = cloned {
assert_eq!(src, "src.txt");
assert_eq!(dst, "dst.txt");
} else {
panic!("Expected Copy operation");
}
}
#[test]
fn test_op_move_clone() {
let op = Op::Move {
src: "old.txt".to_string(),
dst: "new.txt".to_string(),
};
let cloned = op.clone();
if let Op::Move { src, dst } = cloned {
assert_eq!(src, "old.txt");
assert_eq!(dst, "new.txt");
} else {
panic!("Expected Move operation");
}
}
#[test]
fn test_op_delete_clone() {
let op = Op::Delete {
path: "file.txt".to_string(),
recursive: true,
};
let cloned = op.clone();
if let Op::Delete { path, recursive } = cloned {
assert_eq!(path, "file.txt");
assert!(recursive);
} else {
panic!("Expected Delete operation");
}
}
#[test]
fn test_op_write_clone() {
let op = Op::Write {
path: "file.txt".to_string(),
content: b"content".to_vec(),
};
let cloned = op.clone();
if let Op::Write { path, content } = cloned {
assert_eq!(path, "file.txt");
assert_eq!(content, b"content");
} else {
panic!("Expected Write operation");
}
}
#[test]
fn test_op_symlink_clone() {
let op = Op::Symlink {
link_path: "link".to_string(),
target: "target".to_string(),
};
let cloned = op.clone();
if let Op::Symlink { link_path, target } = cloned {
assert_eq!(link_path, "link");
assert_eq!(target, "target");
} else {
panic!("Expected Symlink operation");
}
}
#[test]
fn test_op_chmod_clone() {
let op = Op::Chmod {
path: "file.sh".to_string(),
permissions: Permissions::executable(),
recursive: false,
};
let cloned = op.clone();
if let Op::Chmod {
path,
permissions,
recursive,
} = cloned
{
assert_eq!(path, "file.sh");
assert_eq!(permissions.to_octal(), 0o755);
assert!(!recursive);
} else {
panic!("Expected Chmod operation");
}
}
#[test]
fn test_op_make_executable_clone() {
let op = Op::MakeExecutable {
path: "script.sh".to_string(),
};
let cloned = op.clone();
if let Op::MakeExecutable { path } = cloned {
assert_eq!(path, "script.sh");
} else {
panic!("Expected MakeExecutable operation");
}
}
#[test]
fn test_op_debug_format() {
let op = Op::Copy {
src: "a".to_string(),
dst: "b".to_string(),
};
let debug_str = format!("{:?}", op);
assert!(debug_str.contains("Copy"));
assert!(debug_str.contains("a"));
assert!(debug_str.contains("b"));
}
#[test]
fn test_preview_debug_format() {
let preview = Preview {
base_commit: "abc".to_string(),
base_file_count: 5,
operations: vec![],
};
let debug_str = format!("{:?}", preview);
assert!(debug_str.contains("Preview"));
assert!(debug_str.contains("abc"));
}
use std::fs as std_fs;
use tempfile::TempDir;
fn create_test_repo() -> (TempDir, crate::repo::Repository) {
let tmp = TempDir::new().unwrap();
let repo_path = tmp.path().join("test.forge");
let repo = crate::repo::Repository::init(&repo_path).unwrap();
repo.commit_internal(
&[
("README.md", b"# Test Project\n"),
("src/main.rs", b"fn main() {}\n"),
("src/lib.rs", b"pub fn hello() {}\n"),
("config.json", b"{\"key\": \"value\"}\n"),
],
"Initial commit",
"test_author",
None,
None,
)
.unwrap();
(tmp, repo)
}
#[test]
fn test_upload_single_file() {
let (tmp, repo) = create_test_repo();
let os_file = tmp.path().join("upload_test.txt");
std_fs::write(&os_file, b"uploaded content").unwrap();
let hash = upload(&repo, &os_file, "uploaded.txt", "uploader", None).unwrap();
assert!(!hash.is_empty());
let tip = repo.branch_tip_internal("trunk").unwrap();
let content = repo.read_file_internal(&tip.hash, "uploaded.txt").unwrap();
assert_eq!(content, b"uploaded content");
}
#[test]
fn test_upload_with_custom_message() {
let (tmp, repo) = create_test_repo();
let os_file = tmp.path().join("custom_msg.txt");
std_fs::write(&os_file, b"content").unwrap();
let hash = upload(
&repo,
&os_file,
"custom.txt",
"author",
Some("Custom upload message"),
)
.unwrap();
assert!(!hash.is_empty());
let tip = repo.branch_tip_internal("trunk").unwrap();
let content = repo.read_file_internal(&tip.hash, "custom.txt").unwrap();
assert_eq!(content, b"content");
}
#[test]
fn test_upload_nonexistent_file() {
let (tmp, repo) = create_test_repo();
let result = upload(
&repo,
tmp.path().join("nonexistent.txt"),
"dest.txt",
"author",
None,
);
assert!(result.is_err());
}
#[test]
fn test_upload_dir() {
let (tmp, repo) = create_test_repo();
let upload_dir_path = tmp.path().join("to_upload");
std_fs::create_dir_all(upload_dir_path.join("subdir")).unwrap();
std_fs::write(upload_dir_path.join("file1.txt"), b"content1").unwrap();
std_fs::write(upload_dir_path.join("file2.txt"), b"content2").unwrap();
std_fs::write(upload_dir_path.join("subdir/nested.txt"), b"nested").unwrap();
let hash = upload_dir(&repo, &upload_dir_path, "imported", "author", None).unwrap();
assert!(!hash.is_empty());
let tip = repo.branch_tip_internal("trunk").unwrap();
let content1 = repo
.read_file_internal(&tip.hash, "imported/file1.txt")
.unwrap();
assert_eq!(content1, b"content1");
let content2 = repo
.read_file_internal(&tip.hash, "imported/file2.txt")
.unwrap();
assert_eq!(content2, b"content2");
let nested = repo
.read_file_internal(&tip.hash, "imported/subdir/nested.txt")
.unwrap();
assert_eq!(nested, b"nested");
}
#[test]
fn test_upload_dir_not_a_directory() {
let (tmp, repo) = create_test_repo();
let file_path = tmp.path().join("regular_file.txt");
std_fs::write(&file_path, b"content").unwrap();
let result = upload_dir(&repo, &file_path, "dest", "author", None);
assert!(result.is_err());
}
#[test]
fn test_download_single_file() {
let (tmp, repo) = create_test_repo();
let dest_path = tmp.path().join("downloaded.md");
download(&repo, "README.md", &dest_path).unwrap();
let content = std_fs::read_to_string(&dest_path).unwrap();
assert_eq!(content, "# Test Project\n");
}
#[test]
fn test_download_creates_parent_dirs() {
let (tmp, repo) = create_test_repo();
let dest_path = tmp.path().join("deep/nested/path/config.json");
download(&repo, "config.json", &dest_path).unwrap();
assert!(dest_path.exists());
let content = std_fs::read_to_string(&dest_path).unwrap();
assert_eq!(content, "{\"key\": \"value\"}\n");
}
#[test]
fn test_download_nonexistent_file() {
let (tmp, repo) = create_test_repo();
let result = download(&repo, "nonexistent.txt", tmp.path().join("out.txt"));
assert!(result.is_err());
}
#[test]
fn test_download_dir() {
let (tmp, repo) = create_test_repo();
let dest_dir = tmp.path().join("exported_src");
let count = download_dir(&repo, "src", &dest_dir).unwrap();
assert_eq!(count, 2);
let main_content = std_fs::read_to_string(dest_dir.join("main.rs")).unwrap();
assert_eq!(main_content, "fn main() {}\n");
let lib_content = std_fs::read_to_string(dest_dir.join("lib.rs")).unwrap();
assert_eq!(lib_content, "pub fn hello() {}\n");
}
#[test]
fn test_download_dir_entire_repo() {
let (tmp, repo) = create_test_repo();
let dest_dir = tmp.path().join("full_export");
let count = download_dir(&repo, "", &dest_dir).unwrap();
assert_eq!(count, 4);
assert!(dest_dir.join("README.md").exists());
assert!(dest_dir.join("config.json").exists());
assert!(dest_dir.join("src/main.rs").exists());
assert!(dest_dir.join("src/lib.rs").exists());
}
#[test]
fn test_download_matching() {
let (tmp, repo) = create_test_repo();
let dest_dir = tmp.path().join("rust_files");
let count = download_matching(&repo, "**/*.rs", &dest_dir).unwrap();
assert_eq!(count, 2);
assert!(dest_dir.join("src/main.rs").exists());
assert!(dest_dir.join("src/lib.rs").exists());
}
#[test]
fn test_download_matching_no_matches() {
let (tmp, repo) = create_test_repo();
let dest_dir = tmp.path().join("no_matches");
let count = download_matching(&repo, "**/*.py", &dest_dir).unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_upload_then_download_roundtrip() {
let (tmp, repo) = create_test_repo();
let original_content = b"This is test content for roundtrip!";
let original_path = tmp.path().join("original.txt");
std_fs::write(&original_path, original_content).unwrap();
upload(&repo, &original_path, "roundtrip.txt", "author", None).unwrap();
let downloaded_path = tmp.path().join("downloaded.txt");
download(&repo, "roundtrip.txt", &downloaded_path).unwrap();
let downloaded_content = std_fs::read(&downloaded_path).unwrap();
assert_eq!(downloaded_content, original_content);
}
#[test]
fn test_upload_overwrites_existing() {
let (tmp, repo) = create_test_repo();
let os_file = tmp.path().join("new_readme.md");
std_fs::write(&os_file, b"# Updated README\n").unwrap();
upload(&repo, &os_file, "README.md", "author", None).unwrap();
let tip = repo.branch_tip_internal("trunk").unwrap();
let content = repo.read_file_internal(&tip.hash, "README.md").unwrap();
assert_eq!(content, b"# Updated README\n");
}
#[test]
fn test_upload_creates_new_commit() {
let (tmp, repo) = create_test_repo();
let initial_tip = repo.branch_tip_internal("trunk").unwrap();
let os_file = tmp.path().join("new_file.txt");
std_fs::write(&os_file, b"new content").unwrap();
upload(&repo, &os_file, "new.txt", "author", None).unwrap();
let new_tip = repo.branch_tip_internal("trunk").unwrap();
assert_ne!(initial_tip.hash, new_tip.hash);
}
#[test]
fn test_upload_dir_empty_repo_path() {
let (tmp, repo) = create_test_repo();
let upload_path = tmp.path().join("root_upload");
std_fs::create_dir_all(&upload_path).unwrap();
std_fs::write(upload_path.join("root_file.txt"), b"at root").unwrap();
upload_dir(&repo, &upload_path, "", "author", None).unwrap();
let tip = repo.branch_tip_internal("trunk").unwrap();
let content = repo.read_file_internal(&tip.hash, "root_file.txt").unwrap();
assert_eq!(content, b"at root");
}
}