error_handling/
error_handling.rs

1//! Error Handling Example
2//!
3//! This example demonstrates comprehensive error handling patterns:
4//! - Handle GitError variants (IoError, CommandFailed)
5//! - Recovery strategies for common error scenarios
6//! - Best practices for error propagation
7//! - Graceful degradation when operations fail
8//!
9//! Run with: cargo run --example error_handling
10
11use rustic_git::{GitError, Repository, Result};
12use std::{env, fs};
13
14fn main() -> Result<()> {
15    println!("Rustic Git - Error Handling Example\n");
16
17    let base_path = env::temp_dir().join("rustic_git_error_example");
18    let repo_path = base_path.join("test_repo");
19
20    // Clean up any previous runs
21    if base_path.exists() {
22        fs::remove_dir_all(&base_path).expect("Failed to clean up previous example");
23    }
24    fs::create_dir_all(&base_path).expect("Failed to create base directory");
25
26    println!("=== GitError Types and Handling ===\n");
27
28    // Demonstrate different error types and handling strategies
29    demonstrate_repository_errors(&repo_path)?;
30    demonstrate_file_operation_errors(&repo_path)?;
31    demonstrate_git_command_errors(&repo_path)?;
32    demonstrate_error_recovery_patterns(&repo_path)?;
33    demonstrate_error_propagation_strategies(&base_path)?;
34
35    // Clean up
36    println!("Cleaning up error handling examples...");
37    fs::remove_dir_all(&base_path)?;
38    println!("Error handling example completed successfully!");
39
40    Ok(())
41}
42
43/// Demonstrate repository-related errors
44fn demonstrate_repository_errors(repo_path: &std::path::Path) -> Result<()> {
45    println!("Repository Error Scenarios:\n");
46
47    // 1. Opening non-existent repository
48    println!("1. Attempting to open non-existent repository:");
49    match Repository::open("/definitely/does/not/exist") {
50        Ok(_) => println!("   Unexpectedly succeeded"),
51        Err(GitError::IoError(msg)) => {
52            println!("   IoError caught: {}", msg);
53            println!("   This typically happens when the path doesn't exist");
54        }
55        Err(GitError::CommandFailed(msg)) => {
56            println!("   CommandFailed caught: {}", msg);
57            println!("   Git command failed - path exists but isn't a repo");
58        }
59    }
60
61    // 2. Opening a file as a repository
62    let fake_repo_path = repo_path.with_extension("fake.txt");
63    fs::write(&fake_repo_path, "This is not a git repository")?;
64
65    println!("\n2. Attempting to open regular file as repository:");
66    match Repository::open(&fake_repo_path) {
67        Ok(_) => println!("   Unexpectedly succeeded"),
68        Err(GitError::CommandFailed(msg)) => {
69            println!("   CommandFailed caught: {}", msg);
70            println!("   Git recognized the path but it's not a repository");
71        }
72        Err(GitError::IoError(msg)) => {
73            println!("   IoError caught: {}", msg);
74        }
75    }
76
77    fs::remove_file(&fake_repo_path)?;
78
79    // 3. Initializing repository with invalid path
80    println!("\n3. Attempting to initialize repository with problematic path:");
81
82    // Try to initialize in a location that might cause issues
83    match Repository::init("/root/definitely_no_permission", false) {
84        Ok(_) => println!("   Unexpectedly succeeded (you might be running as root!)"),
85        Err(GitError::IoError(msg)) => {
86            println!("   IoError caught: {}", msg);
87            println!("   Likely a permission issue");
88        }
89        Err(GitError::CommandFailed(msg)) => {
90            println!("   CommandFailed caught: {}", msg);
91            println!("   Git init command failed");
92        }
93    }
94
95    println!();
96    Ok(())
97}
98
99/// Demonstrate file operation related errors
100fn demonstrate_file_operation_errors(repo_path: &std::path::Path) -> Result<()> {
101    println!("File Operation Error Scenarios:\n");
102
103    // Set up a valid repository first
104    let repo = Repository::init(repo_path, false)?;
105
106    // Create some test files
107    fs::write(repo_path.join("test.txt"), "Test content")?;
108    repo.add(&["test.txt"])?;
109    repo.commit("Initial commit")?;
110
111    // 1. Adding non-existent files
112    println!("1. Attempting to add non-existent files:");
113    match repo.add(&["does_not_exist.txt", "also_missing.txt"]) {
114        Ok(_) => println!("   Unexpectedly succeeded"),
115        Err(GitError::CommandFailed(msg)) => {
116            println!("   CommandFailed caught: {}", msg);
117            println!("   Git add failed because files don't exist");
118        }
119        Err(GitError::IoError(msg)) => {
120            println!("   IoError caught: {}", msg);
121        }
122    }
123
124    // 2. Mixed valid and invalid files
125    println!("\n2. Adding mix of valid and invalid files:");
126    fs::write(repo_path.join("valid.txt"), "Valid file")?;
127
128    match repo.add(&["valid.txt", "invalid.txt"]) {
129        Ok(_) => {
130            println!("   Partially succeeded - some Git versions allow this");
131            // Check what actually got staged
132            let status = repo.status()?;
133            println!("   {} files staged despite error", status.entries.len());
134        }
135        Err(GitError::CommandFailed(msg)) => {
136            println!("   CommandFailed caught: {}", msg);
137            println!("   Entire add operation failed due to invalid file");
138
139            // Try recovery: add valid files individually
140            println!("   Recovery: Adding valid files individually...");
141            match repo.add(&["valid.txt"]) {
142                Ok(_) => println!("      Successfully added valid.txt"),
143                Err(e) => println!("      Recovery failed: {:?}", e),
144            }
145        }
146        Err(GitError::IoError(msg)) => {
147            println!("   IoError caught: {}", msg);
148        }
149    }
150
151    println!();
152    Ok(())
153}
154
155/// Demonstrate Git command related errors
156fn demonstrate_git_command_errors(repo_path: &std::path::Path) -> Result<()> {
157    println!("Git Command Error Scenarios:\n");
158
159    let repo = Repository::open(repo_path)?;
160
161    // 1. Empty commit (no staged changes)
162    println!("1. Attempting commit with no staged changes:");
163    match repo.commit("Empty commit attempt") {
164        Ok(hash) => {
165            println!("   Unexpectedly succeeded: {}", hash.short());
166            println!("   Some Git configurations allow empty commits");
167        }
168        Err(GitError::CommandFailed(msg)) => {
169            println!("   CommandFailed caught: {}", msg);
170            println!("   Git requires changes to commit (normal behavior)");
171        }
172        Err(GitError::IoError(msg)) => {
173            println!("   IoError caught: {}", msg);
174        }
175    }
176
177    // 2. Commit with problematic message
178    println!("\n2. Testing commit message edge cases:");
179
180    // Stage a file for testing
181    fs::write(
182        repo_path.join("commit_test.txt"),
183        "Content for commit testing",
184    )?;
185    repo.add(&["commit_test.txt"])?;
186
187    // Very long commit message
188    let very_long_message = "A ".repeat(1000) + "very long commit message";
189    match repo.commit(&very_long_message) {
190        Ok(hash) => {
191            println!("   Long commit message succeeded: {}", hash.short());
192            println!("   Git handled the long message fine");
193        }
194        Err(GitError::CommandFailed(msg)) => {
195            println!("   Long commit message failed: {}", msg);
196        }
197        Err(GitError::IoError(msg)) => {
198            println!("   IoError with long message: {}", msg);
199        }
200    }
201
202    println!();
203    Ok(())
204}
205
206/// Demonstrate error recovery patterns
207fn demonstrate_error_recovery_patterns(repo_path: &std::path::Path) -> Result<()> {
208    println!("Error Recovery Patterns:\n");
209
210    let repo = Repository::open(repo_path)?;
211
212    // Pattern 1: Retry with different approach
213    println!("1. Retry Pattern - Graceful degradation:");
214
215    // Try to add specific files, fall back to add_all on failure
216    let files_to_add = ["missing1.txt", "missing2.txt", "missing3.txt"];
217
218    println!("   Attempting to add specific files...");
219    match repo.add(&files_to_add) {
220        Ok(_) => println!("      Specific files added successfully"),
221        Err(e) => {
222            println!("      Specific files failed: {:?}", e);
223            println!("      Falling back to add_all()...");
224
225            match repo.add_all() {
226                Ok(_) => {
227                    let status = repo.status()?;
228                    println!(
229                        "      add_all() succeeded, {} files staged",
230                        status.entries.len()
231                    );
232                }
233                Err(fallback_error) => {
234                    println!("      Fallback also failed: {:?}", fallback_error);
235                }
236            }
237        }
238    }
239
240    // Pattern 2: Partial success handling
241    println!("\n2. Partial Success Pattern:");
242
243    // Create some files with known issues
244    fs::write(repo_path.join("good1.txt"), "Good file 1")?;
245    fs::write(repo_path.join("good2.txt"), "Good file 2")?;
246    // Don't create bad1.txt - it will be missing
247
248    let mixed_files = ["good1.txt", "bad1.txt", "good2.txt"];
249
250    println!("   Attempting to add mixed valid/invalid files...");
251    match repo.add(&mixed_files) {
252        Ok(_) => println!("      All files added (unexpected success)"),
253        Err(GitError::CommandFailed(msg)) => {
254            println!("      Batch add failed: {}", msg);
255            println!("      Recovery: Adding files individually...");
256
257            let mut successful_adds = 0;
258            let mut failed_adds = 0;
259
260            for file in &mixed_files {
261                match repo.add(&[file]) {
262                    Ok(_) => {
263                        successful_adds += 1;
264                        println!("         Added: {}", file);
265                    }
266                    Err(_) => {
267                        failed_adds += 1;
268                        println!("         Failed: {}", file);
269                    }
270                }
271            }
272
273            println!(
274                "      Results: {} succeeded, {} failed",
275                successful_adds, failed_adds
276            );
277        }
278        Err(GitError::IoError(msg)) => {
279            println!("      IoError during batch add: {}", msg);
280        }
281    }
282
283    // Pattern 3: Status checking before operations
284    println!("\n3. Preventive Pattern - Check before operation:");
285
286    println!("   Checking repository status before commit...");
287    let status = repo.status()?;
288
289    if status.is_clean() {
290        println!("      Repository is clean - no commit needed");
291    } else {
292        println!("      Repository has {} changes", status.entries.len());
293
294        // Show what would be committed
295        for entry in &status.entries {
296            println!(
297                "         Index {:?}, Worktree {:?}: {}",
298                entry.index_status,
299                entry.worktree_status,
300                entry.path.display()
301            );
302        }
303
304        // Safe commit since we know there are changes
305        match repo.commit("Commit after status check") {
306            Ok(hash) => println!("      Safe commit succeeded: {}", hash.short()),
307            Err(e) => println!("      Even safe commit failed: {:?}", e),
308        }
309    }
310
311    println!();
312    Ok(())
313}
314
315/// Demonstrate error propagation strategies
316fn demonstrate_error_propagation_strategies(base_path: &std::path::Path) -> Result<()> {
317    println!("Error Propagation Strategies:\n");
318
319    // Strategy 1: Early return with ?
320    println!("1. Early Return Strategy (using ?):");
321    match workflow_with_early_return(base_path) {
322        Ok(message) => println!("      Workflow completed: {}", message),
323        Err(e) => println!("      Workflow failed early: {:?}", e),
324    }
325
326    // Strategy 2: Collect all errors
327    println!("\n2. Error Collection Strategy:");
328    let results = workflow_with_error_collection(base_path);
329
330    let successful = results.iter().filter(|r| r.is_ok()).count();
331    let failed = results.iter().filter(|r| r.is_err()).count();
332
333    println!(
334        "      Operations: {} succeeded, {} failed",
335        successful, failed
336    );
337
338    for (i, result) in results.iter().enumerate() {
339        match result {
340            Ok(msg) => println!("         Step {}: {}", i + 1, msg),
341            Err(e) => println!("         Step {}: {:?}", i + 1, e),
342        }
343    }
344
345    // Strategy 3: Error context enrichment
346    println!("\n3. Error Context Strategy:");
347    match workflow_with_context(base_path) {
348        Ok(message) => println!("      Contextual workflow: {}", message),
349        Err(e) => println!("      Contextual workflow failed: {:?}", e),
350    }
351
352    println!();
353    Ok(())
354}
355
356/// Workflow that returns early on first error
357fn workflow_with_early_return(base_path: &std::path::Path) -> Result<String> {
358    let repo_path = base_path.join("early_return_test");
359
360    // This will propagate any error immediately
361    let repo = Repository::init(&repo_path, false)?;
362
363    fs::write(repo_path.join("file1.txt"), "Content 1")?;
364    repo.add(&["file1.txt"])?;
365
366    let hash = repo.commit("Early return workflow commit")?;
367
368    // Clean up
369    fs::remove_dir_all(&repo_path)?;
370
371    Ok(format!("Completed with commit {}", hash.short()))
372}
373
374/// Workflow that collects all errors instead of failing fast
375fn workflow_with_error_collection(base_path: &std::path::Path) -> Vec<Result<String>> {
376    let repo_path = base_path.join("error_collection_test");
377    let mut results = Vec::new();
378
379    // Step 1: Initialize repo
380    results.push(Repository::init(&repo_path, false).map(|_| "Repository initialized".to_string()));
381
382    // Step 2: Add files (some may fail)
383    let files_to_create = ["good.txt", "another_good.txt"];
384
385    for file in &files_to_create {
386        results.push(
387            fs::write(repo_path.join(file), "Content")
388                .map_err(GitError::from)
389                .map(|_| format!("Created {}", file)),
390        );
391    }
392
393    // Step 3: Try to add files (continue even if repo init failed)
394    if let Ok(repo) = Repository::open(&repo_path) {
395        results.push(
396            repo.add(&files_to_create)
397                .map(|_| "Files added to staging".to_string()),
398        );
399
400        results.push(
401            repo.commit("Error collection workflow")
402                .map(|hash| format!("Committed: {}", hash.short())),
403        );
404    } else {
405        results.push(Err(GitError::CommandFailed(
406            "Could not open repo for adding files".to_string(),
407        )));
408        results.push(Err(GitError::CommandFailed(
409            "Could not open repo for commit".to_string(),
410        )));
411    }
412
413    // Cleanup (don't add to results as it's not part of main workflow)
414    let _ = fs::remove_dir_all(&repo_path);
415
416    results
417}
418
419/// Workflow with enhanced error context
420fn workflow_with_context(base_path: &std::path::Path) -> Result<String> {
421    let repo_path = base_path.join("context_test");
422
423    // Add context to errors
424    let repo = Repository::init(&repo_path, false).inspect_err(|_e| {
425        eprintln!(
426            "Context: Failed to initialize repository at {}",
427            repo_path.display()
428        );
429    })?;
430
431    // Create file with context
432    fs::write(repo_path.join("context_file.txt"), "Content with context").map_err(|e| {
433        eprintln!("Context: Failed to create context_file.txt");
434        GitError::from(e)
435    })?;
436
437    // Add with context
438    repo.add(&["context_file.txt"]).inspect_err(|_e| {
439        eprintln!("Context: Failed to stage context_file.txt");
440    })?;
441
442    // Commit with context
443    let hash = repo.commit("Context workflow commit").inspect_err(|_e| {
444        eprintln!("Context: Failed to create commit");
445    })?;
446
447    // Clean up
448    fs::remove_dir_all(&repo_path)?;
449
450    Ok(format!("Context workflow completed: {}", hash.short()))
451}