Skip to content

Commit a8ecb4b

Browse files
committed
feat: add comprehensive diff operations API
Implement multi-level diff API with DiffOutput, FileDiff, and DiffOptions types. Add Repository methods for unstaged, staged, commit, and configurable diffs. Include comprehensive example and update documentation.
1 parent e126137 commit a8ecb4b

File tree

8 files changed

+1384
-9
lines changed

8 files changed

+1384
-9
lines changed

CLAUDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
- Author struct: name, email, timestamp with Display implementation
5757
- CommitMessage: subject and optional body parsing
5858
- CommitDetails: full commit info including file changes and diff stats
59-
- **Core types**: Hash (in src/types.rs), IndexStatus, WorktreeStatus, FileEntry (in src/commands/status.rs), Branch, BranchList, BranchType (in src/commands/branch.rs), Commit, CommitLog, Author, CommitMessage, CommitDetails, LogOptions (in src/commands/log.rs), RepoConfig (in src/commands/config.rs), Remote, RemoteList, FetchOptions, PushOptions (in src/commands/remote.rs), RestoreOptions, RemoveOptions, MoveOptions (in src/commands/files.rs)
59+
- **Core types**: Hash (in src/types.rs), IndexStatus, WorktreeStatus, FileEntry (in src/commands/status.rs), Branch, BranchList, BranchType (in src/commands/branch.rs), Commit, CommitLog, Author, CommitMessage, CommitDetails, LogOptions (in src/commands/log.rs), RepoConfig (in src/commands/config.rs), Remote, RemoteList, FetchOptions, PushOptions (in src/commands/remote.rs), RestoreOptions, RemoveOptions, MoveOptions (in src/commands/files.rs), DiffOutput, FileDiff, DiffStatus, DiffOptions, DiffStats, DiffChunk, DiffLine, DiffLineType (in src/commands/diff.rs)
6060
- **Utility functions**: git(args, working_dir) -> Result<String>, git_raw(args, working_dir) -> Result<Output>
6161
- **Remote management**: Full remote operations with network support
6262
- Repository::add_remote(name, url) -> Result<()> - add remote repository
@@ -87,8 +87,20 @@
8787
- RestoreOptions: with_source(), with_staged(), with_worktree() - builder for restore configuration
8888
- RemoveOptions: with_force(), with_recursive(), with_cached(), with_ignore_unmatch() - builder for remove configuration
8989
- MoveOptions: with_force(), with_verbose(), with_dry_run() - builder for move configuration
90-
- **Command modules**: status.rs, add.rs, commit.rs, branch.rs, log.rs, config.rs, remote.rs, files.rs (in src/commands/)
91-
- **Testing**: 128+ tests covering all functionality with comprehensive edge cases
90+
- **Diff operations**: Multi-level API for comprehensive change comparison
91+
- Repository::diff() -> Result<DiffOutput> - working directory vs index (unstaged changes)
92+
- Repository::diff_staged() -> Result<DiffOutput> - index vs HEAD (staged changes)
93+
- Repository::diff_head() -> Result<DiffOutput> - working directory vs HEAD (all changes)
94+
- Repository::diff_commits(from, to) -> Result<DiffOutput> - between specific commits
95+
- Repository::diff_with_options(options) -> Result<DiffOutput> - advanced diff with DiffOptions
96+
- DiffOutput: files, stats with immutable collections and comprehensive filtering
97+
- FileDiff: path, old_path, status, chunks, additions, deletions with change details
98+
- DiffStatus enum: Added, Modified, Deleted, Renamed, Copied with const char conversion
99+
- DiffOptions: context_lines, whitespace handling, path filtering, output formats (name-only, stat, numstat)
100+
- DiffStats: files_changed, insertions, deletions with aggregate statistics
101+
- Complete filtering: files_with_status(), iter(), is_empty(), len() for result analysis
102+
- **Command modules**: status.rs, add.rs, commit.rs, branch.rs, log.rs, config.rs, remote.rs, files.rs, diff.rs (in src/commands/)
103+
- **Testing**: 144+ tests covering all functionality with comprehensive edge cases
92104
- Run `cargo fmt && cargo build && cargo test && cargo clippy --all-targets --all-features -- -D warnings` after code changes
93105
- Make sure all examples are running
94106

@@ -105,6 +117,7 @@ The `examples/` directory contains comprehensive demonstrations of library funct
105117
- **config_operations.rs**: Repository configuration management - user setup, configuration values, repository-scoped settings
106118
- **remote_operations.rs**: Complete remote management - add/remove/rename remotes, fetch/push operations with options, network operations, error handling
107119
- **file_lifecycle_operations.rs**: Comprehensive file management - restore/reset/remove/move operations, .gitignore management, advanced file lifecycle workflows, staging area manipulation
120+
- **diff_operations.rs**: Comprehensive diff operations showcase - unstaged/staged diffs, commit comparisons, advanced options (whitespace handling, path filtering), output formats (name-only, stat, numstat), and change analysis
108121
- **error_handling.rs**: Comprehensive error handling patterns - GitError variants, recovery strategies
109122

110123
Run examples with: `cargo run --example <example_name>`

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rustic-git"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2024"
55
license = "MIT"
66
description = "A Rustic Git - clean type-safe API over git cli"

README.md

Lines changed: 254 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ Rustic Git provides a simple, ergonomic interface for common Git operations. It
2929
-**Remote management** with full CRUD operations and network support
3030
-**Network operations** (fetch, push, clone) with advanced options
3131
-**File lifecycle operations** (restore, reset, remove, move, .gitignore management)
32+
-**Diff operations** with multi-level API and comprehensive options
3233
- ✅ Type-safe error handling with custom GitError enum
3334
- ✅ Universal `Hash` type for Git objects
3435
-**Immutable collections** (Box<[T]>) for memory efficiency
3536
-**Const enum conversions** with zero runtime cost
36-
- ✅ Comprehensive test coverage (128+ tests)
37+
- ✅ Comprehensive test coverage (144+ tests)
3738

3839
## Installation
3940

@@ -53,7 +54,7 @@ cargo add rustic-git
5354
## Quick Start
5455

5556
```rust
56-
use rustic_git::{Repository, Result, IndexStatus, WorktreeStatus, LogOptions, FetchOptions, PushOptions, RestoreOptions, RemoveOptions, MoveOptions};
57+
use rustic_git::{Repository, Result, IndexStatus, WorktreeStatus, LogOptions, FetchOptions, PushOptions, RestoreOptions, RemoveOptions, MoveOptions, DiffOptions, DiffOutput, DiffStatus};
5758

5859
fn main() -> Result<()> {
5960
// Initialize a new repository
@@ -164,6 +165,41 @@ fn main() -> Result<()> {
164165
let is_ignored = repo.ignore_check("temp_file.tmp")?;
165166
let patterns = repo.ignore_list()?;
166167

168+
// Diff operations
169+
// Check for unstaged changes
170+
let diff = repo.diff()?;
171+
if !diff.is_empty() {
172+
println!("Unstaged changes found:");
173+
for file in diff.iter() {
174+
println!(" {} {}", file.status, file.path.display());
175+
}
176+
}
177+
178+
// Check for staged changes
179+
let staged_diff = repo.diff_staged()?;
180+
println!("Files staged for commit: {}", staged_diff.len());
181+
182+
// Compare between commits
183+
let recent_commits = repo.recent_commits(2)?;
184+
if recent_commits.len() >= 2 {
185+
let commit_diff = repo.diff_commits(
186+
&recent_commits.iter().nth(1).unwrap().hash,
187+
&recent_commits.iter().nth(0).unwrap().hash,
188+
)?;
189+
println!("Changes in last commit: {}", commit_diff.stats);
190+
}
191+
192+
// Diff with options
193+
let diff_opts = DiffOptions::new()
194+
.ignore_whitespace()
195+
.context_lines(5);
196+
let detailed_diff = repo.diff_with_options(&diff_opts)?;
197+
198+
// Filter by status
199+
let added_files: Vec<_> = detailed_diff.files_with_status(DiffStatus::Added).collect();
200+
let modified_files: Vec<_> = detailed_diff.files_with_status(DiffStatus::Modified).collect();
201+
println!("Added: {} files, Modified: {} files", added_files.len(), modified_files.len());
202+
167203
Ok(())
168204
}
169205
```
@@ -1127,6 +1163,218 @@ fn main() -> rustic_git::Result<()> {
11271163
}
11281164
```
11291165

1166+
### Diff Operations
1167+
1168+
The diff operations provide a comprehensive API for comparing different states in your Git repository. All diff operations return a `DiffOutput` containing file changes and statistics.
1169+
1170+
#### `Repository::diff() -> Result<DiffOutput>`
1171+
1172+
Get differences between working directory and index (unstaged changes).
1173+
1174+
```rust
1175+
let diff = repo.diff()?;
1176+
1177+
if diff.is_empty() {
1178+
println!("No unstaged changes");
1179+
} else {
1180+
println!("Unstaged changes in {} files:", diff.len());
1181+
for file in diff.iter() {
1182+
println!(" {} {} (+{} -{} lines)",
1183+
file.status,
1184+
file.path.display(),
1185+
file.additions,
1186+
file.deletions);
1187+
}
1188+
println!("{}", diff.stats);
1189+
}
1190+
```
1191+
1192+
#### `Repository::diff_staged() -> Result<DiffOutput>`
1193+
1194+
Get differences between index and HEAD (staged changes).
1195+
1196+
```rust
1197+
let staged_diff = repo.diff_staged()?;
1198+
println!("Files staged for commit: {}", staged_diff.len());
1199+
1200+
// Filter by change type
1201+
let added_files: Vec<_> = staged_diff.files_with_status(DiffStatus::Added).collect();
1202+
let modified_files: Vec<_> = staged_diff.files_with_status(DiffStatus::Modified).collect();
1203+
let deleted_files: Vec<_> = staged_diff.files_with_status(DiffStatus::Deleted).collect();
1204+
1205+
println!("Staged changes: {} added, {} modified, {} deleted",
1206+
added_files.len(), modified_files.len(), deleted_files.len());
1207+
```
1208+
1209+
#### `Repository::diff_head() -> Result<DiffOutput>`
1210+
1211+
Get all differences between working directory and HEAD (both staged and unstaged).
1212+
1213+
```rust
1214+
let head_diff = repo.diff_head()?;
1215+
println!("All changes since last commit:");
1216+
for file in head_diff.iter() {
1217+
println!(" {} {}", file.status, file.path.display());
1218+
}
1219+
```
1220+
1221+
#### `Repository::diff_commits(from, to) -> Result<DiffOutput>`
1222+
1223+
Compare two specific commits.
1224+
1225+
```rust
1226+
let commits = repo.recent_commits(2)?;
1227+
if commits.len() >= 2 {
1228+
let diff = repo.diff_commits(&commits[1].hash, &commits[0].hash)?;
1229+
println!("Changes in last commit:");
1230+
println!(" {}", diff.stats);
1231+
1232+
// Show renames and copies
1233+
for file in diff.iter() {
1234+
match file.status {
1235+
DiffStatus::Renamed => {
1236+
if let Some(old_path) = &file.old_path {
1237+
println!(" Renamed: {} -> {}", old_path.display(), file.path.display());
1238+
}
1239+
},
1240+
DiffStatus::Copied => {
1241+
if let Some(old_path) = &file.old_path {
1242+
println!(" Copied: {} -> {}", old_path.display(), file.path.display());
1243+
}
1244+
},
1245+
_ => println!(" {} {}", file.status, file.path.display()),
1246+
}
1247+
}
1248+
}
1249+
```
1250+
1251+
#### `Repository::diff_with_options(options) -> Result<DiffOutput>`
1252+
1253+
Advanced diff operations with custom options.
1254+
1255+
```rust
1256+
// Diff with custom options
1257+
let options = DiffOptions::new()
1258+
.ignore_whitespace() // Ignore whitespace changes
1259+
.ignore_whitespace_change() // Ignore whitespace amount changes
1260+
.ignore_blank_lines() // Ignore blank line changes
1261+
.context_lines(10) // Show 10 lines of context
1262+
.paths(vec![PathBuf::from("src/")]); // Only diff src/ directory
1263+
1264+
let diff = repo.diff_with_options(&options)?;
1265+
1266+
// Different output formats
1267+
let name_only = repo.diff_with_options(&DiffOptions::new().name_only())?;
1268+
println!("Changed files:");
1269+
for file in name_only.iter() {
1270+
println!(" {}", file.path.display());
1271+
}
1272+
1273+
let stat_diff = repo.diff_with_options(&DiffOptions::new().stat_only())?;
1274+
println!("Diff statistics:\n{}", stat_diff);
1275+
1276+
let numstat_diff = repo.diff_with_options(&DiffOptions::new().numstat())?;
1277+
for file in numstat_diff.iter() {
1278+
println!("{}\t+{}\t-{}", file.path.display(), file.additions, file.deletions);
1279+
}
1280+
```
1281+
1282+
#### Diff Types and Data Structures
1283+
1284+
```rust
1285+
// Main diff output containing files and statistics
1286+
pub struct DiffOutput {
1287+
pub files: Box<[FileDiff]>, // Immutable collection of file changes
1288+
pub stats: DiffStats, // Aggregate statistics
1289+
}
1290+
1291+
// Individual file changes
1292+
pub struct FileDiff {
1293+
pub path: PathBuf, // Current file path
1294+
pub old_path: Option<PathBuf>, // Original path (for renames/copies)
1295+
pub status: DiffStatus, // Type of change
1296+
pub chunks: Box<[DiffChunk]>, // Diff chunks (for full diff parsing)
1297+
pub additions: usize, // Lines added
1298+
pub deletions: usize, // Lines deleted
1299+
}
1300+
1301+
// Change status for files
1302+
pub enum DiffStatus {
1303+
Added, // New file
1304+
Modified, // Changed file
1305+
Deleted, // Removed file
1306+
Renamed, // File renamed
1307+
Copied, // File copied
1308+
}
1309+
1310+
// Aggregate statistics
1311+
pub struct DiffStats {
1312+
pub files_changed: usize,
1313+
pub insertions: usize,
1314+
pub deletions: usize,
1315+
}
1316+
```
1317+
1318+
#### Diff Options Builder
1319+
1320+
```rust
1321+
// Build custom diff options
1322+
let options = DiffOptions::new()
1323+
.context_lines(5) // Lines of context around changes
1324+
.ignore_whitespace() // --ignore-all-space
1325+
.ignore_whitespace_change() // --ignore-space-change
1326+
.ignore_blank_lines() // --ignore-blank-lines
1327+
.name_only() // Show only file names
1328+
.stat_only() // Show only statistics
1329+
.numstat() // Show numerical statistics
1330+
.cached() // Compare index with HEAD
1331+
.no_index() // Compare files outside git
1332+
.paths(vec![PathBuf::from("src/")]); // Limit to specific paths
1333+
1334+
let diff = repo.diff_with_options(&options)?;
1335+
```
1336+
1337+
#### Working with Diff Results
1338+
1339+
```rust
1340+
let diff = repo.diff()?;
1341+
1342+
// Check if any changes exist
1343+
if diff.is_empty() {
1344+
println!("No changes");
1345+
return Ok(());
1346+
}
1347+
1348+
// Iterate over all changed files
1349+
for file in diff.iter() {
1350+
println!("{} {}", file.status, file.path.display());
1351+
1352+
// Check if file is binary
1353+
if file.is_binary() {
1354+
println!(" (binary file)");
1355+
continue;
1356+
}
1357+
1358+
// Show change statistics
1359+
println!(" +{} -{} lines", file.additions, file.deletions);
1360+
}
1361+
1362+
// Filter by specific change types
1363+
let new_files: Vec<_> = diff.files_with_status(DiffStatus::Added).collect();
1364+
let modified_files: Vec<_> = diff.files_with_status(DiffStatus::Modified).collect();
1365+
let deleted_files: Vec<_> = diff.files_with_status(DiffStatus::Deleted).collect();
1366+
1367+
println!("Summary: {} new, {} modified, {} deleted",
1368+
new_files.len(), modified_files.len(), deleted_files.len());
1369+
1370+
// Access aggregate statistics
1371+
println!("Total: {}", diff.stats);
1372+
println!("Files: {}, +{} insertions, -{} deletions",
1373+
diff.stats.files_changed,
1374+
diff.stats.insertions,
1375+
diff.stats.deletions);
1376+
```
1377+
11301378
## Examples
11311379

11321380
The `examples/` directory contains comprehensive demonstrations of library functionality:
@@ -1164,6 +1412,9 @@ cargo run --example remote_operations
11641412
# File lifecycle operations (restore, remove, move, .gitignore)
11651413
cargo run --example file_lifecycle_operations
11661414

1415+
# Diff operations with multi-level API and comprehensive options
1416+
cargo run --example diff_operations
1417+
11671418
# Error handling patterns and recovery strategies
11681419
cargo run --example error_handling
11691420
```
@@ -1179,6 +1430,7 @@ cargo run --example error_handling
11791430
- **`config_operations.rs`** - Repository configuration management demonstration: user setup, configuration values, and repository-scoped settings
11801431
- **`commit_history.rs`** - Comprehensive commit history & log operations showing all querying APIs, filtering, analysis, and advanced LogOptions usage
11811432
- **`remote_operations.rs`** - Complete remote management demonstration: add, remove, rename remotes, fetch/push operations with options, and network operations
1433+
- **`diff_operations.rs`** - Comprehensive diff operations showcase: unstaged/staged diffs, commit comparisons, advanced options, filtering, and output formats
11821434
- **`file_lifecycle_operations.rs`** - Comprehensive file management demonstration: restore, reset, remove, move operations, .gitignore management, and advanced file lifecycle workflows
11831435
- **`error_handling.rs`** - Comprehensive error handling patterns showing GitError variants, recovery strategies, and best practices
11841436

0 commit comments

Comments
 (0)