A tool to help managing forked repos with their own history
at main 337 lines 11 kB view raw
1//! Integration tests for forkme 2 3use std::fs; 4use tempfile::TempDir; 5 6use forkme::config::{Config, Upstream}; 7use forkme::git::FileContent; 8use forkme::patch::{self, PatchEntry}; 9use git2::Repository; 10 11/// Helper to create a test git repository using git2 12fn create_test_repo(dir: &std::path::Path) -> Repository { 13 let repo = Repository::init(dir).unwrap(); 14 15 // Configure repo 16 let mut config = repo.config().unwrap(); 17 config.set_str("user.email", "test@test.com").unwrap(); 18 config.set_str("user.name", "Test User").unwrap(); 19 20 repo 21} 22 23/// Helper to commit files to a repo 24fn commit_files(repo: &Repository, files: &[(&str, &str)], message: &str) { 25 let mut index = repo.index().unwrap(); 26 27 for (path, content) in files { 28 let full_path = repo.workdir().unwrap().join(path); 29 if let Some(parent) = full_path.parent() { 30 fs::create_dir_all(parent).unwrap(); 31 } 32 fs::write(&full_path, content).unwrap(); 33 index.add_path(std::path::Path::new(path)).unwrap(); 34 } 35 36 index.write().unwrap(); 37 let tree_id = index.write_tree().unwrap(); 38 let tree = repo.find_tree(tree_id).unwrap(); 39 40 let sig = repo.signature().unwrap(); 41 42 // Get parent commit if exists 43 let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok()); 44 45 match parent { 46 Some(p) => { 47 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&p]) 48 .unwrap(); 49 } 50 None => { 51 repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[]) 52 .unwrap(); 53 } 54 } 55} 56 57#[test] 58fn test_patch_generation_and_application() { 59 let original = "line1\nline2\nline3\n"; 60 let modified = "line1\nmodified_line2\nline3\nnew_line4\n"; 61 62 // Generate patch 63 let patch_content = patch::generate_patch(Some(original), Some(modified)); 64 65 // Verify patch contains expected changes 66 assert!(patch_content.contains("-line2")); 67 assert!(patch_content.contains("+modified_line2")); 68 assert!(patch_content.contains("+new_line4")); 69 70 // Apply patch 71 let result = patch::apply_patch(original, &patch_content).unwrap(); 72 assert_eq!(result, modified); 73} 74 75#[test] 76fn test_config_roundtrip() { 77 let temp_dir = TempDir::new().unwrap(); 78 let config_path = temp_dir.path().join("forkme.toml"); 79 80 let config = Config { 81 upstream: Upstream { 82 url: "https://github.com/example/repo.git".to_string(), 83 branch: "main".to_string(), 84 }, 85 }; 86 87 config.save_to(&config_path).unwrap(); 88 89 let loaded = Config::load_from(&config_path).unwrap(); 90 assert_eq!(loaded.upstream.url, config.upstream.url); 91 assert_eq!(loaded.upstream.branch, config.upstream.branch); 92} 93 94#[test] 95fn test_patch_entry_types() { 96 let text = PatchEntry::TextPatch("src/lib.rs".to_string()); 97 let binary = PatchEntry::Binary("assets/logo.png".to_string()); 98 let deleted = PatchEntry::Deleted("old_file.rs".to_string()); 99 100 assert_eq!(text.file_path(), "src/lib.rs"); 101 assert_eq!(binary.file_path(), "assets/logo.png"); 102 assert_eq!(deleted.file_path(), "old_file.rs"); 103} 104 105#[test] 106fn test_new_file_patch() { 107 // Create a patch for a completely new file 108 let new_content = "pub fn new_function() {\n // todo\n}\n"; 109 let patch_content = patch::generate_patch(None, Some(new_content)); 110 111 // Should indicate this is a new file (starts from nothing) 112 assert!(patch_content.contains("@@ -0,0")); 113 114 // Apply to empty string should work 115 let result = patch::apply_patch("", &patch_content).unwrap(); 116 assert_eq!(result, new_content); 117} 118 119#[test] 120fn test_file_deletion_patch() { 121 let old_content = "this file will be deleted\n"; 122 let patch_content = patch::generate_patch(Some(old_content), None); 123 124 // Should indicate deletion 125 assert!(patch_content.contains("-this file will be deleted")); 126} 127 128#[test] 129fn test_file_content_text_detection() { 130 let text = FileContent::Text("hello world".to_string()); 131 assert!(!text.is_binary()); 132 assert_eq!(text.as_text(), Some("hello world")); 133} 134 135#[test] 136fn test_file_content_binary_detection() { 137 let binary = FileContent::Binary(vec![0x89, 0x50, 0x4E, 0x47]); // PNG magic bytes 138 assert!(binary.is_binary()); 139 assert_eq!(binary.as_text(), None); 140 assert_eq!(binary.as_bytes(), &[0x89, 0x50, 0x4E, 0x47]); 141} 142 143#[test] 144fn test_create_repo_with_git2() { 145 let temp_dir = TempDir::new().unwrap(); 146 let repo = create_test_repo(temp_dir.path()); 147 148 // Create initial commit 149 commit_files( 150 &repo, 151 &[("README.md", "# Test\n"), ("src/main.rs", "fn main() {}\n")], 152 "Initial commit", 153 ); 154 155 // Verify commit exists 156 let head = repo.head().unwrap(); 157 let commit = head.peel_to_commit().unwrap(); 158 assert_eq!(commit.message(), Some("Initial commit")); 159 160 // Verify files exist 161 assert!(temp_dir.path().join("README.md").exists()); 162 assert!(temp_dir.path().join("src/main.rs").exists()); 163} 164 165#[test] 166fn test_repo_diff_detection() { 167 let temp_dir = TempDir::new().unwrap(); 168 let repo = create_test_repo(temp_dir.path()); 169 170 // Create initial commit 171 commit_files(&repo, &[("file.txt", "original\n")], "Initial"); 172 173 // Create second commit with changes 174 commit_files(&repo, &[("file.txt", "modified\n")], "Modified"); 175 176 // Get the two commits 177 let head = repo.head().unwrap().peel_to_commit().unwrap(); 178 let parent = head.parent(0).unwrap(); 179 180 // Diff between commits 181 let old_tree = parent.tree().unwrap(); 182 let new_tree = head.tree().unwrap(); 183 let diff = repo 184 .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) 185 .unwrap(); 186 187 // Should have one file changed 188 assert_eq!(diff.deltas().count(), 1); 189} 190 191/// End-to-end test simulating the forkme workflow: 192/// 1. Create upstream repo with initial commit on main 193/// 2. Clone and create forkme branch 194/// 3. Make changes on forkme branch 195/// 4. Use library functions to generate patches 196/// 5. Verify patches can be applied to restore changes 197#[test] 198fn test_full_sync_workflow() { 199 // Create "upstream" repo 200 let upstream_dir = TempDir::new().unwrap(); 201 let upstream_repo = create_test_repo(upstream_dir.path()); 202 203 // Initial commit on main 204 commit_files( 205 &upstream_repo, 206 &[ 207 ("README.md", "# Original Project\n"), 208 ("src/lib.rs", "pub fn original() {}\n"), 209 ], 210 "Initial commit", 211 ); 212 213 // Create "local" repo (simulating clone) 214 let local_dir = TempDir::new().unwrap(); 215 let local_repo = 216 Repository::clone(upstream_dir.path().to_str().unwrap(), local_dir.path()).unwrap(); 217 218 // Configure local repo 219 let mut config = local_repo.config().unwrap(); 220 config.set_str("user.email", "test@test.com").unwrap(); 221 config.set_str("user.name", "Test User").unwrap(); 222 223 // Create forkme branch from origin/main (which is now called origin/master or main) 224 let head = local_repo.head().unwrap(); 225 let head_commit = head.peel_to_commit().unwrap(); 226 local_repo.branch("forkme", &head_commit, false).unwrap(); 227 228 // Checkout forkme branch 229 let obj = local_repo.revparse_single("refs/heads/forkme").unwrap(); 230 local_repo.checkout_tree(&obj, None).unwrap(); 231 local_repo.set_head("refs/heads/forkme").unwrap(); 232 233 // Make changes on forkme branch 234 commit_files( 235 &local_repo, 236 &[ 237 ("README.md", "# Modified Project\n\nWith extra content.\n"), 238 ( 239 "src/lib.rs", 240 "pub fn original() {}\n\npub fn new_function() {\n // added by fork\n}\n", 241 ), 242 ( 243 "src/new_file.rs", 244 "// Completely new file\npub fn fork_only() {}\n", 245 ), 246 ], 247 "Fork modifications", 248 ); 249 250 // Now simulate what sync does: get changes between upstream and forkme 251 // First, find the upstream commit (origin/master or origin/main) 252 let upstream_ref = local_repo 253 .find_reference("refs/remotes/origin/master") 254 .or_else(|_| local_repo.find_reference("refs/remotes/origin/main")) 255 .unwrap(); 256 let upstream_commit = upstream_ref.peel_to_commit().unwrap(); 257 let upstream_tree = upstream_commit.tree().unwrap(); 258 259 let forkme_head = local_repo.head().unwrap().peel_to_commit().unwrap(); 260 let forkme_tree = forkme_head.tree().unwrap(); 261 262 // Get diff 263 let diff = local_repo 264 .diff_tree_to_tree(Some(&upstream_tree), Some(&forkme_tree), None) 265 .unwrap(); 266 267 // Should have 3 files changed 268 assert_eq!(diff.deltas().count(), 3); 269 270 // Collect changes and generate patches 271 let mut patches: Vec<(String, String)> = Vec::new(); 272 273 diff.foreach( 274 &mut |delta, _| { 275 let path = delta 276 .new_file() 277 .path() 278 .unwrap() 279 .to_string_lossy() 280 .to_string(); 281 282 let old_content = if delta.old_file().id().is_zero() { 283 None 284 } else { 285 local_repo 286 .find_blob(delta.old_file().id()) 287 .ok() 288 .and_then(|blob| String::from_utf8(blob.content().to_vec()).ok()) 289 }; 290 291 let new_content = if delta.new_file().id().is_zero() { 292 None 293 } else { 294 local_repo 295 .find_blob(delta.new_file().id()) 296 .ok() 297 .and_then(|blob| String::from_utf8(blob.content().to_vec()).ok()) 298 }; 299 300 let patch_content = 301 patch::generate_patch(old_content.as_deref(), new_content.as_deref()); 302 patches.push((path, patch_content)); 303 true 304 }, 305 None, 306 None, 307 None, 308 ) 309 .unwrap(); 310 311 // Verify we have patches for all 3 files 312 assert_eq!(patches.len(), 3); 313 314 // Verify README patch contains the changes 315 let readme_patch = patches.iter().find(|(p, _)| p == "README.md").unwrap(); 316 assert!(readme_patch.1.contains("-# Original Project")); 317 assert!(readme_patch.1.contains("+# Modified Project")); 318 319 // Verify new file patch 320 let new_file_patch = patches 321 .iter() 322 .find(|(p, _)| p == "src/new_file.rs") 323 .unwrap(); 324 assert!(new_file_patch.1.contains("@@ -0,0")); // New file indicator 325 326 // Test applying patches to original content restores the fork changes 327 let original_readme = "# Original Project\n"; 328 let patched_readme = patch::apply_patch(original_readme, &readme_patch.1).unwrap(); 329 assert_eq!( 330 patched_readme, 331 "# Modified Project\n\nWith extra content.\n" 332 ); 333 334 // Test new file can be created from patch 335 let new_file_content = patch::apply_patch("", &new_file_patch.1).unwrap(); 336 assert!(new_file_content.contains("pub fn fork_only()")); 337}