rustic-git 0.6.0

A Rustic Git - clean type-safe API over git cli
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! Error Handling Example
//!
//! This example demonstrates comprehensive error handling patterns:
//! - Handle GitError variants (IoError, CommandFailed)
//! - Recovery strategies for common error scenarios
//! - Best practices for error propagation
//! - Graceful degradation when operations fail
//!
//! Run with: cargo run --example error_handling

use rustic_git::{GitError, Repository, Result};
use std::{env, fs};

fn main() -> Result<()> {
    println!("Rustic Git - Error Handling Example\n");

    let base_path = env::temp_dir().join("rustic_git_error_example");
    let repo_path = base_path.join("test_repo");

    // Clean up any previous runs
    if base_path.exists() {
        fs::remove_dir_all(&base_path).expect("Failed to clean up previous example");
    }
    fs::create_dir_all(&base_path).expect("Failed to create base directory");

    println!("=== GitError Types and Handling ===\n");

    // Demonstrate different error types and handling strategies
    demonstrate_repository_errors(&repo_path)?;
    demonstrate_file_operation_errors(&repo_path)?;
    demonstrate_git_command_errors(&repo_path)?;
    demonstrate_error_recovery_patterns(&repo_path)?;
    demonstrate_error_propagation_strategies(&base_path)?;

    // Clean up
    println!("Cleaning up error handling examples...");
    fs::remove_dir_all(&base_path)?;
    println!("Error handling example completed successfully!");

    Ok(())
}

/// Demonstrate repository-related errors
fn demonstrate_repository_errors(repo_path: &std::path::Path) -> Result<()> {
    println!("Repository Error Scenarios:\n");

    // 1. Opening non-existent repository
    println!("1. Attempting to open non-existent repository:");
    match Repository::open("/definitely/does/not/exist") {
        Ok(_) => println!("   Unexpectedly succeeded"),
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
            println!("   This typically happens when the path doesn't exist");
        }
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Git command failed - path exists but isn't a repo");
        }
    }

    // 2. Opening a file as a repository
    let fake_repo_path = repo_path.with_extension("fake.txt");
    fs::write(&fake_repo_path, "This is not a git repository")?;

    println!("\n2. Attempting to open regular file as repository:");
    match Repository::open(&fake_repo_path) {
        Ok(_) => println!("   Unexpectedly succeeded"),
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Git recognized the path but it's not a repository");
        }
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
        }
    }

    fs::remove_file(&fake_repo_path)?;

    // 3. Initializing repository with invalid path
    println!("\n3. Attempting to initialize repository with problematic path:");

    // Try to initialize in a location that might cause issues
    match Repository::init("/root/definitely_no_permission", false) {
        Ok(_) => println!("   Unexpectedly succeeded (you might be running as root!)"),
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
            println!("   Likely a permission issue");
        }
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Git init command failed");
        }
    }

    println!();
    Ok(())
}

/// Demonstrate file operation related errors
fn demonstrate_file_operation_errors(repo_path: &std::path::Path) -> Result<()> {
    println!("File Operation Error Scenarios:\n");

    // Set up a valid repository first
    let repo = Repository::init(repo_path, false)?;

    // Create some test files
    fs::write(repo_path.join("test.txt"), "Test content")?;
    repo.add(&["test.txt"])?;
    repo.commit("Initial commit")?;

    // 1. Adding non-existent files
    println!("1. Attempting to add non-existent files:");
    match repo.add(&["does_not_exist.txt", "also_missing.txt"]) {
        Ok(_) => println!("   Unexpectedly succeeded"),
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Git add failed because files don't exist");
        }
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
        }
    }

    // 2. Mixed valid and invalid files
    println!("\n2. Adding mix of valid and invalid files:");
    fs::write(repo_path.join("valid.txt"), "Valid file")?;

    match repo.add(&["valid.txt", "invalid.txt"]) {
        Ok(_) => {
            println!("   Partially succeeded - some Git versions allow this");
            // Check what actually got staged
            let status = repo.status()?;
            println!("   {} files staged despite error", status.entries.len());
        }
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Entire add operation failed due to invalid file");

            // Try recovery: add valid files individually
            println!("   Recovery: Adding valid files individually...");
            match repo.add(&["valid.txt"]) {
                Ok(_) => println!("      Successfully added valid.txt"),
                Err(e) => println!("      Recovery failed: {:?}", e),
            }
        }
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
        }
    }

    println!();
    Ok(())
}

/// Demonstrate Git command related errors
fn demonstrate_git_command_errors(repo_path: &std::path::Path) -> Result<()> {
    println!("Git Command Error Scenarios:\n");

    let repo = Repository::open(repo_path)?;

    // 1. Empty commit (no staged changes)
    println!("1. Attempting commit with no staged changes:");
    match repo.commit("Empty commit attempt") {
        Ok(hash) => {
            println!("   Unexpectedly succeeded: {}", hash.short());
            println!("   Some Git configurations allow empty commits");
        }
        Err(GitError::CommandFailed(msg)) => {
            println!("   CommandFailed caught: {}", msg);
            println!("   Git requires changes to commit (normal behavior)");
        }
        Err(GitError::IoError(msg)) => {
            println!("   IoError caught: {}", msg);
        }
    }

    // 2. Commit with problematic message
    println!("\n2. Testing commit message edge cases:");

    // Stage a file for testing
    fs::write(
        repo_path.join("commit_test.txt"),
        "Content for commit testing",
    )?;
    repo.add(&["commit_test.txt"])?;

    // Very long commit message
    let very_long_message = "A ".repeat(1000) + "very long commit message";
    match repo.commit(&very_long_message) {
        Ok(hash) => {
            println!("   Long commit message succeeded: {}", hash.short());
            println!("   Git handled the long message fine");
        }
        Err(GitError::CommandFailed(msg)) => {
            println!("   Long commit message failed: {}", msg);
        }
        Err(GitError::IoError(msg)) => {
            println!("   IoError with long message: {}", msg);
        }
    }

    println!();
    Ok(())
}

/// Demonstrate error recovery patterns
fn demonstrate_error_recovery_patterns(repo_path: &std::path::Path) -> Result<()> {
    println!("Error Recovery Patterns:\n");

    let repo = Repository::open(repo_path)?;

    // Pattern 1: Retry with different approach
    println!("1. Retry Pattern - Graceful degradation:");

    // Try to add specific files, fall back to add_all on failure
    let files_to_add = ["missing1.txt", "missing2.txt", "missing3.txt"];

    println!("   Attempting to add specific files...");
    match repo.add(&files_to_add) {
        Ok(_) => println!("      Specific files added successfully"),
        Err(e) => {
            println!("      Specific files failed: {:?}", e);
            println!("      Falling back to add_all()...");

            match repo.add_all() {
                Ok(_) => {
                    let status = repo.status()?;
                    println!(
                        "      add_all() succeeded, {} files staged",
                        status.entries.len()
                    );
                }
                Err(fallback_error) => {
                    println!("      Fallback also failed: {:?}", fallback_error);
                }
            }
        }
    }

    // Pattern 2: Partial success handling
    println!("\n2. Partial Success Pattern:");

    // Create some files with known issues
    fs::write(repo_path.join("good1.txt"), "Good file 1")?;
    fs::write(repo_path.join("good2.txt"), "Good file 2")?;
    // Don't create bad1.txt - it will be missing

    let mixed_files = ["good1.txt", "bad1.txt", "good2.txt"];

    println!("   Attempting to add mixed valid/invalid files...");
    match repo.add(&mixed_files) {
        Ok(_) => println!("      All files added (unexpected success)"),
        Err(GitError::CommandFailed(msg)) => {
            println!("      Batch add failed: {}", msg);
            println!("      Recovery: Adding files individually...");

            let mut successful_adds = 0;
            let mut failed_adds = 0;

            for file in &mixed_files {
                match repo.add(&[file]) {
                    Ok(_) => {
                        successful_adds += 1;
                        println!("         Added: {}", file);
                    }
                    Err(_) => {
                        failed_adds += 1;
                        println!("         Failed: {}", file);
                    }
                }
            }

            println!(
                "      Results: {} succeeded, {} failed",
                successful_adds, failed_adds
            );
        }
        Err(GitError::IoError(msg)) => {
            println!("      IoError during batch add: {}", msg);
        }
    }

    // Pattern 3: Status checking before operations
    println!("\n3. Preventive Pattern - Check before operation:");

    println!("   Checking repository status before commit...");
    let status = repo.status()?;

    if status.is_clean() {
        println!("      Repository is clean - no commit needed");
    } else {
        println!("      Repository has {} changes", status.entries.len());

        // Show what would be committed
        for entry in &status.entries {
            println!(
                "         Index {:?}, Worktree {:?}: {}",
                entry.index_status,
                entry.worktree_status,
                entry.path.display()
            );
        }

        // Safe commit since we know there are changes
        match repo.commit("Commit after status check") {
            Ok(hash) => println!("      Safe commit succeeded: {}", hash.short()),
            Err(e) => println!("      Even safe commit failed: {:?}", e),
        }
    }

    println!();
    Ok(())
}

/// Demonstrate error propagation strategies
fn demonstrate_error_propagation_strategies(base_path: &std::path::Path) -> Result<()> {
    println!("Error Propagation Strategies:\n");

    // Strategy 1: Early return with ?
    println!("1. Early Return Strategy (using ?):");
    match workflow_with_early_return(base_path) {
        Ok(message) => println!("      Workflow completed: {}", message),
        Err(e) => println!("      Workflow failed early: {:?}", e),
    }

    // Strategy 2: Collect all errors
    println!("\n2. Error Collection Strategy:");
    let results = workflow_with_error_collection(base_path);

    let successful = results.iter().filter(|r| r.is_ok()).count();
    let failed = results.iter().filter(|r| r.is_err()).count();

    println!(
        "      Operations: {} succeeded, {} failed",
        successful, failed
    );

    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(msg) => println!("         Step {}: {}", i + 1, msg),
            Err(e) => println!("         Step {}: {:?}", i + 1, e),
        }
    }

    // Strategy 3: Error context enrichment
    println!("\n3. Error Context Strategy:");
    match workflow_with_context(base_path) {
        Ok(message) => println!("      Contextual workflow: {}", message),
        Err(e) => println!("      Contextual workflow failed: {:?}", e),
    }

    println!();
    Ok(())
}

/// Workflow that returns early on first error
fn workflow_with_early_return(base_path: &std::path::Path) -> Result<String> {
    let repo_path = base_path.join("early_return_test");

    // This will propagate any error immediately
    let repo = Repository::init(&repo_path, false)?;

    fs::write(repo_path.join("file1.txt"), "Content 1")?;
    repo.add(&["file1.txt"])?;

    let hash = repo.commit("Early return workflow commit")?;

    // Clean up
    fs::remove_dir_all(&repo_path)?;

    Ok(format!("Completed with commit {}", hash.short()))
}

/// Workflow that collects all errors instead of failing fast
fn workflow_with_error_collection(base_path: &std::path::Path) -> Vec<Result<String>> {
    let repo_path = base_path.join("error_collection_test");
    let mut results = Vec::new();

    // Step 1: Initialize repo
    results.push(Repository::init(&repo_path, false).map(|_| "Repository initialized".to_string()));

    // Step 2: Add files (some may fail)
    let files_to_create = ["good.txt", "another_good.txt"];

    for file in &files_to_create {
        results.push(
            fs::write(repo_path.join(file), "Content")
                .map_err(GitError::from)
                .map(|_| format!("Created {}", file)),
        );
    }

    // Step 3: Try to add files (continue even if repo init failed)
    if let Ok(repo) = Repository::open(&repo_path) {
        results.push(
            repo.add(&files_to_create)
                .map(|_| "Files added to staging".to_string()),
        );

        results.push(
            repo.commit("Error collection workflow")
                .map(|hash| format!("Committed: {}", hash.short())),
        );
    } else {
        results.push(Err(GitError::CommandFailed(
            "Could not open repo for adding files".to_string(),
        )));
        results.push(Err(GitError::CommandFailed(
            "Could not open repo for commit".to_string(),
        )));
    }

    // Cleanup (don't add to results as it's not part of main workflow)
    let _ = fs::remove_dir_all(&repo_path);

    results
}

/// Workflow with enhanced error context
fn workflow_with_context(base_path: &std::path::Path) -> Result<String> {
    let repo_path = base_path.join("context_test");

    // Add context to errors
    let repo = Repository::init(&repo_path, false).inspect_err(|_e| {
        eprintln!(
            "Context: Failed to initialize repository at {}",
            repo_path.display()
        );
    })?;

    // Create file with context
    fs::write(repo_path.join("context_file.txt"), "Content with context").map_err(|e| {
        eprintln!("Context: Failed to create context_file.txt");
        GitError::from(e)
    })?;

    // Add with context
    repo.add(&["context_file.txt"]).inspect_err(|_e| {
        eprintln!("Context: Failed to stage context_file.txt");
    })?;

    // Commit with context
    let hash = repo.commit("Context workflow commit").inspect_err(|_e| {
        eprintln!("Context: Failed to create commit");
    })?;

    // Clean up
    fs::remove_dir_all(&repo_path)?;

    Ok(format!("Context workflow completed: {}", hash.short()))
}