staging_operations/
staging_operations.rs

1//! Staging Operations Example
2//!
3//! This example demonstrates all available staging methods:
4//! - add(): Stage specific files
5//! - add_all(): Stage all changes (like `git add .`)
6//! - add_update(): Stage all tracked file changes (like `git add -u`)
7//! - Show before/after status for each operation
8//!
9//! Run with: cargo run --example staging_operations
10
11use rustic_git::{IndexStatus, Repository, Result, WorktreeStatus};
12use std::{env, fs};
13
14fn main() -> Result<()> {
15    println!("Rustic Git - Staging Operations Example\n");
16
17    let repo_path = env::temp_dir().join("rustic_git_staging_example");
18
19    // Clean up any previous run
20    if repo_path.exists() {
21        fs::remove_dir_all(&repo_path).expect("Failed to clean up previous example");
22    }
23
24    // Initialize repository and create initial commit
25    println!("Setting up repository with initial files...");
26    let repo = Repository::init(&repo_path, false)?;
27
28    // Create initial files
29    fs::create_dir_all(repo_path.join("src"))?;
30    fs::create_dir_all(repo_path.join("docs"))?;
31
32    fs::write(
33        repo_path.join("README.md"),
34        "# Staging Demo\nOriginal content",
35    )?;
36    fs::write(
37        repo_path.join("src/main.rs"),
38        "fn main() { println!(\"v1\"); }",
39    )?;
40    fs::write(
41        repo_path.join("src/lib.rs"),
42        "pub fn version() -> &'static str { \"1.0\" }",
43    )?;
44
45    // Create initial commit so we can demonstrate staging tracked file changes
46    repo.add_all()?;
47    let _initial_hash = repo.commit("Initial commit with basic files")?;
48    println!("Created initial repository with 3 files\n");
49
50    println!("=== Staging Specific Files with add() ===\n");
51
52    // Create some new files and modify existing ones
53    println!("Creating new files and modifying existing ones...");
54    fs::write(repo_path.join("new_file1.txt"), "New file 1 content")?;
55    fs::write(repo_path.join("new_file2.txt"), "New file 2 content")?;
56    fs::write(repo_path.join("docs/guide.md"), "# User Guide")?;
57
58    // Modify existing files
59    fs::write(
60        repo_path.join("README.md"),
61        "# Staging Demo\nUpdated content!",
62    )?;
63    fs::write(
64        repo_path.join("src/main.rs"),
65        "fn main() { println!(\"v2 - updated!\"); }",
66    )?;
67
68    println!("Created 3 new files and modified 2 existing files");
69
70    // Show status before staging
71    println!("\nStatus before staging:");
72    let status_before = repo.status()?;
73    display_status_breakdown(&status_before);
74
75    // Stage specific files using add()
76    println!("\nUsing add() to stage specific files:");
77
78    // Stage just the README.md
79    repo.add(&["README.md"])?;
80    println!("   Staged README.md");
81
82    let status_after_readme = repo.status()?;
83    display_status_changes(
84        &status_before,
85        &status_after_readme,
86        "after staging README.md",
87    );
88
89    // Stage multiple specific files
90    repo.add(&["new_file1.txt", "src/main.rs"])?;
91    println!("   Staged new_file1.txt and src/main.rs");
92
93    let status_after_multiple = repo.status()?;
94    display_status_changes(
95        &status_after_readme,
96        &status_after_multiple,
97        "after staging multiple files",
98    );
99
100    // Stage using Path objects (alternative syntax)
101    use std::path::Path as StdPath;
102    repo.add(&[StdPath::new("docs/guide.md")])?;
103    println!("   Staged docs/guide.md using Path object");
104
105    let status_after_path = repo.status()?;
106    display_status_changes(
107        &status_after_multiple,
108        &status_after_path,
109        "after staging with Path object",
110    );
111
112    println!();
113
114    println!("=== Staging All Changes with add_all() ===\n");
115
116    // Create more files to demonstrate add_all()
117    println!("Creating additional files for add_all() demo...");
118    fs::write(
119        repo_path.join("config.toml"),
120        "[package]\nname = \"example\"",
121    )?;
122    fs::write(repo_path.join("src/utils.rs"), "pub fn helper() {}")?;
123    fs::create_dir_all(repo_path.join("tests"))?;
124    fs::write(
125        repo_path.join("tests/integration.rs"),
126        "#[test]\nfn test_basic() {}",
127    )?;
128
129    println!("Created 3 more files");
130
131    let status_before_add_all = repo.status()?;
132    println!("\nStatus before add_all():");
133    display_status_breakdown(&status_before_add_all);
134
135    // Use add_all() to stage everything remaining
136    println!("\nUsing add_all() to stage all remaining changes:");
137    repo.add_all()?;
138    println!("   Staged all changes with add_all()");
139
140    let status_after_add_all = repo.status()?;
141    display_status_changes(
142        &status_before_add_all,
143        &status_after_add_all,
144        "after add_all()",
145    );
146
147    // Create a commit to set up for add_update() demo
148    let _commit_hash = repo.commit("Add all new files and modifications")?;
149    println!("   Committed all staged changes\n");
150
151    println!("=== Staging Tracked Changes with add_update() ===\n");
152
153    // Create new untracked files and modify existing tracked files
154    println!("Setting up files for add_update() demonstration...");
155
156    // Create new untracked files (these should NOT be staged by add_update)
157    fs::write(repo_path.join("untracked1.txt"), "This is untracked")?;
158    fs::write(repo_path.join("untracked2.txt"), "Another untracked file")?;
159
160    // Modify existing tracked files (these SHOULD be staged by add_update)
161    fs::write(
162        repo_path.join("README.md"),
163        "# Staging Demo\nContent updated again for add_update demo!",
164    )?;
165    fs::write(
166        repo_path.join("src/lib.rs"),
167        "pub fn version() -> &'static str { \"2.0\" }",
168    )?;
169    fs::write(
170        repo_path.join("config.toml"),
171        "[package]\nname = \"example\"\nversion = \"0.2.0\"",
172    )?;
173
174    println!("Created 2 untracked files and modified 3 tracked files");
175
176    let status_before_add_update = repo.status()?;
177    println!("\nStatus before add_update():");
178    display_status_breakdown(&status_before_add_update);
179
180    // Use add_update() to stage only tracked file changes
181    println!("\nUsing add_update() to stage only tracked file modifications:");
182    repo.add_update()?;
183    println!("   Used add_update() - should stage modified tracked files only");
184
185    let status_after_add_update = repo.status()?;
186    display_status_changes(
187        &status_before_add_update,
188        &status_after_add_update,
189        "after add_update()",
190    );
191
192    // Verify that untracked files are still untracked
193    let remaining_untracked: Vec<_> = status_after_add_update.untracked_entries().collect();
194    if !remaining_untracked.is_empty() {
195        println!("   Untracked files remain untracked (as expected):");
196        for entry in remaining_untracked {
197            println!("      - {}", entry.path.display());
198        }
199    }
200
201    println!();
202
203    println!("=== Error Handling in Staging Operations ===\n");
204
205    // Demonstrate error handling
206    println!("Testing error conditions:");
207
208    // Try to add non-existent files
209    match repo.add(&["nonexistent_file.txt"]) {
210        Ok(_) => println!("   Unexpectedly succeeded adding non-existent file"),
211        Err(e) => println!("   Expected error for non-existent file: {:?}", e),
212    }
213
214    // Try to add empty array (should succeed but do nothing)
215    match repo.add(&[] as &[&str]) {
216        Ok(_) => println!("   Empty add() succeeded (no-op)"),
217        Err(e) => println!("   Empty add() failed: {:?}", e),
218    }
219
220    println!();
221
222    println!("=== Final Repository State ===\n");
223
224    let final_status = repo.status()?;
225    println!("Final repository summary:");
226    display_status_breakdown(&final_status);
227
228    if final_status.has_changes() {
229        let staged_count = final_status.staged_files().count();
230        let untracked_count = final_status.untracked_entries().count();
231
232        println!("\nRepository state:");
233        println!("   {} files staged and ready to commit", staged_count);
234        println!("   {} untracked files not yet added", untracked_count);
235
236        if staged_count > 0 {
237            println!("\n   You could now commit with: repo.commit(\"Your message\")?");
238        }
239    }
240
241    // Clean up
242    println!("\nCleaning up example repository...");
243    fs::remove_dir_all(&repo_path)?;
244    println!("Staging operations example completed!");
245
246    Ok(())
247}
248
249/// Display a breakdown of repository status
250fn display_status_breakdown(status: &rustic_git::GitStatus) {
251    if status.is_clean() {
252        println!("   Repository is clean");
253        return;
254    }
255
256    let mut index_counts = std::collections::HashMap::new();
257    let mut worktree_counts = std::collections::HashMap::new();
258
259    for entry in &status.entries {
260        if !matches!(entry.index_status, IndexStatus::Clean) {
261            *index_counts.entry(&entry.index_status).or_insert(0) += 1;
262        }
263        if !matches!(entry.worktree_status, WorktreeStatus::Clean) {
264            *worktree_counts.entry(&entry.worktree_status).or_insert(0) += 1;
265        }
266    }
267
268    println!("   Index status:");
269    for (index_status, count) in &index_counts {
270        let marker = match index_status {
271            IndexStatus::Modified => "[M]",
272            IndexStatus::Added => "[A]",
273            IndexStatus::Deleted => "[D]",
274            IndexStatus::Renamed => "[R]",
275            IndexStatus::Copied => "[C]",
276            IndexStatus::Clean => "[ ]",
277        };
278        println!("      {} {:?}: {} files", marker, index_status, count);
279    }
280
281    println!("   Worktree status:");
282    for (worktree_status, count) in &worktree_counts {
283        let marker = match worktree_status {
284            WorktreeStatus::Modified => "[M]",
285            WorktreeStatus::Deleted => "[D]",
286            WorktreeStatus::Untracked => "[?]",
287            WorktreeStatus::Ignored => "[I]",
288            WorktreeStatus::Clean => "[ ]",
289        };
290        println!("      {} {:?}: {} files", marker, worktree_status, count);
291    }
292}
293
294/// Display changes between two status states
295fn display_status_changes(
296    before: &rustic_git::GitStatus,
297    after: &rustic_git::GitStatus,
298    description: &str,
299) {
300    println!("\n   Status changes {}:", description);
301
302    let before_count = before.entries.len();
303    let after_count = after.entries.len();
304
305    if before_count == after_count {
306        println!("      Total files unchanged ({} files)", after_count);
307    } else {
308        println!(
309            "      Total files: {} → {} ({:+})",
310            before_count,
311            after_count,
312            after_count as i32 - before_count as i32
313        );
314    }
315
316    // Show status summary
317    let before_staged = before.staged_files().count();
318    let after_staged = after.staged_files().count();
319    let before_untracked = before.untracked_entries().count();
320    let after_untracked = after.untracked_entries().count();
321
322    if before_staged != after_staged {
323        println!(
324            "      Staged files: {} → {} ({:+})",
325            before_staged,
326            after_staged,
327            after_staged as i32 - before_staged as i32
328        );
329    }
330
331    if before_untracked != after_untracked {
332        println!(
333            "      Untracked files: {} → {} ({:+})",
334            before_untracked,
335            after_untracked,
336            after_untracked as i32 - before_untracked as i32
337        );
338    }
339}