Skip to content

Commit 450a0ca

Browse files
committed
feat: implement enhanced status API with staged/unstaged tracking
- Replace simple FileStatus enum with IndexStatus and WorktreeStatus enums - Add FileEntry struct combining PathBuf with both status types - Implement Box<[FileEntry]> for immutable, memory-efficient storage - Add const from_char/to_char methods with zero runtime cost - Provide iterator-based API methods: staged_files(), unstaged_files(), untracked_entries() - Add status filtering by IndexStatus and WorktreeStatus - Update all examples and documentation to showcase new capabilities - Maintain Git-accurate representation of index vs worktree states
1 parent 8a55f43 commit 450a0ca

File tree

13 files changed

+760
-372
lines changed

13 files changed

+760
-372
lines changed

CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@
1919

2020
## Implementation
2121
- Available methods: Repository::init(path, bare), Repository::open(path), Repository::status(), Repository::add(paths), Repository::add_all(), Repository::add_update(), Repository::commit(message), Repository::commit_with_author(message, author)
22-
- Status functionality: GitStatus with FileStatus enum, files as Box<[(FileStatus, String)]>
22+
- **Status functionality**: Enhanced GitStatus API with separate staged/unstaged file tracking
23+
- GitStatus with entries as Box<[FileEntry]> for immutable, efficient storage
24+
- FileEntry contains PathBuf, IndexStatus, and WorktreeStatus for precise Git state representation
25+
- IndexStatus enum: Clean, Modified, Added, Deleted, Renamed, Copied (with const from_char/to_char methods)
26+
- WorktreeStatus enum: Clean, Modified, Deleted, Untracked, Ignored (with const from_char/to_char methods)
27+
- API methods: staged_files(), unstaged_files(), untracked_entries(), files_with_index_status(), files_with_worktree_status()
2328
- Add functionality: Stage specific files, all changes, or tracked file updates
2429
- Commit functionality: Create commits and return Hash of created commit
2530
- Hash type: Universal git object hash representation with short() and Display methods
2631
- Utility functions: git(args, working_dir) -> Result<String>, git_raw(args, working_dir) -> Result<Output>
2732
- Command modules: status.rs, add.rs, commit.rs (in src/commands/)
28-
- Core types: Hash (in src/types.rs)
33+
- Core types: Hash (in src/types.rs), IndexStatus, WorktreeStatus, FileEntry (in src/commands/status.rs)
2934
- Run `cargo fmt && cargo build && cargo test && cargo clippy --all-targets --all-features -- -D warnings` after code changes
3035
- Make sure all examples are running
3136

@@ -34,7 +39,7 @@ The `examples/` directory contains comprehensive demonstrations of library funct
3439

3540
- **basic_usage.rs**: Complete workflow from init to commit - demonstrates fundamental rustic-git usage
3641
- **repository_operations.rs**: Repository lifecycle - init regular/bare repos, open existing repos, error handling
37-
- **status_checking.rs**: GitStatus and FileStatus usage - all status query methods and file state filtering
42+
- **status_checking.rs**: Enhanced GitStatus API usage - staged/unstaged file queries, IndexStatus/WorktreeStatus filtering, comprehensive status analysis
3843
- **staging_operations.rs**: Staging operations - add(), add_all(), add_update() with before/after comparisons
3944
- **commit_workflows.rs**: Commit operations and Hash type - commit(), commit_with_author(), Hash methods
4045
- **error_handling.rs**: Comprehensive error handling patterns - GitError variants, recovery strategies

README.md

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ Rustic Git provides a simple, ergonomic interface for common Git operations. It
99
## Features
1010

1111
- ✅ Repository initialization and opening
12-
- ✅ File status checking with detailed parsing
13-
- ✅ File staging (add files, add all, add updates)
12+
-**Enhanced file status checking** with separate staged/unstaged tracking
13+
-**Precise Git state representation** using IndexStatus and WorktreeStatus enums
14+
- ✅ File staging (add files, add all, add updates)
1415
- ✅ Commit creation with hash return
15-
- ✅ Type-safe error handling
16+
- ✅ Type-safe error handling with custom GitError enum
1617
- ✅ Universal `Hash` type for Git objects
17-
- ✅ Comprehensive test coverage
18+
-**Immutable collections** (Box<[FileEntry]>) for memory efficiency
19+
-**Const enum conversions** with zero runtime cost
20+
- ✅ Comprehensive test coverage (80+ tests)
1821

1922
## Installation
2023

@@ -28,7 +31,7 @@ rustic-git = "0.1.0"
2831
## Quick Start
2932

3033
```rust
31-
use rustic_git::{Repository, Result};
34+
use rustic_git::{Repository, Result, IndexStatus, WorktreeStatus};
3235

3336
fn main() -> Result<()> {
3437
// Initialize a new repository
@@ -37,11 +40,24 @@ fn main() -> Result<()> {
3740
// Or open an existing repository
3841
let repo = Repository::open("/path/to/existing/repo")?;
3942

40-
// Check repository status
43+
// Check repository status with enhanced API
4144
let status = repo.status()?;
4245
if !status.is_clean() {
43-
println!("Modified files: {:?}", status.modified_files());
44-
println!("Untracked files: {:?}", status.untracked_files());
46+
// Get files by staging state
47+
let staged_count = status.staged_files().count();
48+
let unstaged_count = status.unstaged_files().count();
49+
let untracked_count = status.untracked_entries().count();
50+
51+
println!("Repository status:");
52+
println!(" Staged: {} files", staged_count);
53+
println!(" Unstaged: {} files", unstaged_count);
54+
println!(" Untracked: {} files", untracked_count);
55+
56+
// Filter by specific status types
57+
let modified_files: Vec<_> = status
58+
.files_with_worktree_status(WorktreeStatus::Modified)
59+
.collect();
60+
println!(" Modified files: {:?}", modified_files);
4561
}
4662

4763
// Stage files
@@ -85,7 +101,7 @@ let repo = Repository::open("/path/to/existing/repo")?;
85101

86102
#### `Repository::status() -> Result<GitStatus>`
87103

88-
Get the current repository status.
104+
Get the current repository status with enhanced staged/unstaged file tracking.
89105

90106
```rust
91107
let status = repo.status()?;
@@ -97,36 +113,81 @@ if status.is_clean() {
97113
println!("Repository has changes");
98114
}
99115

100-
// Get files by status
101-
let modified = status.modified_files();
102-
let untracked = status.untracked_files();
103-
104-
// Or work with all files directly
105-
for (file_status, filename) in &status.files {
106-
println!("{:?}: {}", file_status, filename);
116+
// Get files by staging state
117+
let staged_files: Vec<_> = status.staged_files().collect();
118+
let unstaged_files: Vec<_> = status.unstaged_files().collect();
119+
let untracked_files: Vec<_> = status.untracked_entries().collect();
120+
121+
// Filter by specific status types
122+
let modified_in_index: Vec<_> = status
123+
.files_with_index_status(IndexStatus::Modified)
124+
.collect();
125+
let modified_in_worktree: Vec<_> = status
126+
.files_with_worktree_status(WorktreeStatus::Modified)
127+
.collect();
128+
129+
// Work with all file entries directly
130+
for entry in status.entries() {
131+
println!("[{}][{}] {}",
132+
entry.index_status.to_char(),
133+
entry.worktree_status.to_char(),
134+
entry.path.display()
135+
);
107136
}
108137
```
109138

110139
The `GitStatus` struct contains:
111-
- `files: Box<[(FileStatus, String)]>` - All files with their status
140+
- `entries: Box<[FileEntry]>` - Immutable collection of file entries
112141
- `is_clean()` - Returns true if no changes
113142
- `has_changes()` - Returns true if any changes exist
114-
- `modified_files()` - Get all modified files
115-
- `untracked_files()` - Get all untracked files
116-
- `files_with_status(status)` - Get files with specific status
143+
- `staged_files()` - Iterator over files with index changes (staged)
144+
- `unstaged_files()` - Iterator over files with worktree changes (unstaged)
145+
- `untracked_entries()` - Iterator over untracked files
146+
- `ignored_files()` - Iterator over ignored files
147+
- `files_with_index_status(status)` - Filter by specific index status
148+
- `files_with_worktree_status(status)` - Filter by specific worktree status
117149

118150
#### File Status Types
119151

152+
The enhanced status API uses separate enums for index (staged) and worktree (unstaged) states:
153+
120154
```rust
121-
pub enum FileStatus {
122-
Modified, // File has been modified
123-
Added, // File has been added to index
124-
Deleted, // File has been deleted
125-
Renamed, // File has been renamed
126-
Copied, // File has been copied
127-
Untracked, // File is not tracked by git
128-
Ignored, // File is ignored by git
155+
// Index (staging area) status
156+
pub enum IndexStatus {
157+
Clean, // No changes in index
158+
Modified, // File modified in index
159+
Added, // File added to index
160+
Deleted, // File deleted in index
161+
Renamed, // File renamed in index
162+
Copied, // File copied in index
163+
}
164+
165+
// Worktree (working directory) status
166+
pub enum WorktreeStatus {
167+
Clean, // No changes in worktree
168+
Modified, // File modified in worktree
169+
Deleted, // File deleted in worktree
170+
Untracked, // File not tracked by git
171+
Ignored, // File ignored by git
129172
}
173+
174+
// File entry combining both states
175+
pub struct FileEntry {
176+
pub path: PathBuf,
177+
pub index_status: IndexStatus,
178+
pub worktree_status: WorktreeStatus,
179+
}
180+
```
181+
182+
Both enums support const character conversion:
183+
```rust
184+
// Convert to/from git porcelain characters
185+
let status = IndexStatus::from_char('M'); // IndexStatus::Modified
186+
let char = status.to_char(); // 'M'
187+
188+
// Display formatting
189+
println!("{}", IndexStatus::Modified); // Prints: M
190+
println!("{}", WorktreeStatus::Untracked); // Prints: ?
130191
```
131192

132193
### Staging Operations
@@ -220,7 +281,7 @@ match repo.commit("message") {
220281
## Complete Workflow Example
221282

222283
```rust
223-
use rustic_git::{Repository, FileStatus};
284+
use rustic_git::{Repository, IndexStatus, WorktreeStatus};
224285
use std::fs;
225286

226287
fn main() -> rustic_git::Result<()> {
@@ -229,23 +290,36 @@ fn main() -> rustic_git::Result<()> {
229290

230291
// Create some files
231292
fs::write("./my-project/README.md", "# My Project")?;
232-
fs::write("./my-project/src/main.rs", "fn main() { println!(\"Hello!\"); }")?;
233293
fs::create_dir_all("./my-project/src")?;
294+
fs::write("./my-project/src/main.rs", "fn main() { println!(\"Hello!\"); }")?;
234295

235-
// Check status
296+
// Check status with enhanced API
236297
let status = repo.status()?;
237-
println!("Found {} untracked files", status.untracked_files().len());
298+
let untracked_count = status.untracked_entries().count();
299+
println!("Found {} untracked files", untracked_count);
300+
301+
// Display detailed status
302+
for entry in status.entries() {
303+
println!("[{}][{}] {}",
304+
entry.index_status.to_char(),
305+
entry.worktree_status.to_char(),
306+
entry.path.display()
307+
);
308+
}
238309

239310
// Stage all files
240311
repo.add_all()?;
241312

242-
// Verify staging
313+
// Verify staging with enhanced API
243314
let status = repo.status()?;
244-
let added_files: Vec<_> = status.files.iter()
245-
.filter(|(s, _)| matches!(s, FileStatus::Added))
246-
.map(|(_, f)| f)
315+
let staged_files: Vec<_> = status.staged_files().collect();
316+
println!("Staged {} files", staged_files.len());
317+
318+
// Show specifically added files
319+
let added_files: Vec<_> = status
320+
.files_with_index_status(IndexStatus::Added)
247321
.collect();
248-
println!("Staged files: {:?}", added_files);
322+
println!("Added files: {:?}", added_files);
249323

250324
// Create initial commit
251325
let hash = repo.commit("Initial commit with project structure")?;
@@ -273,7 +347,7 @@ cargo run --example basic_usage
273347
# Repository lifecycle operations
274348
cargo run --example repository_operations
275349

276-
# Status checking and file state filtering
350+
# Enhanced status API with staged/unstaged tracking
277351
cargo run --example status_checking
278352

279353
# Staging operations (add, add_all, add_update)

examples/basic_usage.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ fn main() -> Result<()> {
6262
println!(" Repository is clean (no changes)");
6363
} else {
6464
println!(" Repository has changes:");
65-
println!(" Modified files: {}", status.modified_files().len());
66-
println!(" Untracked files: {}", status.untracked_files().len());
65+
println!(" Unstaged files: {}", status.unstaged_files().count());
66+
println!(" Untracked files: {}", status.untracked_entries().count());
6767

6868
// Show untracked files
69-
for filename in status.untracked_files() {
70-
println!(" - {}", filename);
69+
for entry in status.untracked_entries() {
70+
println!(" - {}", entry.path.display());
7171
}
7272
}
7373
println!();
@@ -91,10 +91,15 @@ fn main() -> Result<()> {
9191
} else {
9292
println!(
9393
" Files staged for commit: {}",
94-
status_after_staging.files.len()
94+
status_after_staging.entries.len()
9595
);
96-
for (file_status, filename) in &status_after_staging.files {
97-
println!(" {:?}: {}", file_status, filename);
96+
for entry in &status_after_staging.entries {
97+
println!(
98+
" Index {:?}, Worktree {:?}: {}",
99+
entry.index_status,
100+
entry.worktree_status,
101+
entry.path.display()
102+
);
98103
}
99104
}
100105
println!();

examples/commit_workflows.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ See CHANGELOG.md for version history.
324324
} else {
325325
println!(
326326
"Repository has {} uncommitted changes",
327-
final_status.files.len()
327+
final_status.entries.len()
328328
);
329329
}
330330

examples/error_handling.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ fn demonstrate_file_operation_errors(repo_path: &str) -> Result<()> {
131131
println!(" Partially succeeded - some Git versions allow this");
132132
// Check what actually got staged
133133
let status = repo.status()?;
134-
println!(" {} files staged despite error", status.files.len());
134+
println!(" {} files staged despite error", status.entries.len());
135135
}
136136
Err(GitError::CommandFailed(msg)) => {
137137
println!(" CommandFailed caught: {}", msg);
@@ -228,7 +228,7 @@ fn demonstrate_error_recovery_patterns(repo_path: &str) -> Result<()> {
228228
let status = repo.status()?;
229229
println!(
230230
" add_all() succeeded, {} files staged",
231-
status.files.len()
231+
status.entries.len()
232232
);
233233
}
234234
Err(fallback_error) => {
@@ -290,11 +290,16 @@ fn demonstrate_error_recovery_patterns(repo_path: &str) -> Result<()> {
290290
if status.is_clean() {
291291
println!(" Repository is clean - no commit needed");
292292
} else {
293-
println!(" Repository has {} changes", status.files.len());
293+
println!(" Repository has {} changes", status.entries.len());
294294

295295
// Show what would be committed
296-
for (file_status, filename) in &status.files {
297-
println!(" {:?}: {}", file_status, filename);
296+
for entry in &status.entries {
297+
println!(
298+
" Index {:?}, Worktree {:?}: {}",
299+
entry.index_status,
300+
entry.worktree_status,
301+
entry.path.display()
302+
);
298303
}
299304

300305
// Safe commit since we know there are changes

examples/repository_operations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn main() -> Result<()> {
6666

6767
// Test that we can perform operations on the opened repo
6868
let status = opened_repo.status()?;
69-
println!(" Repository status: {} files", status.files.len());
69+
println!(" Repository status: {} files", status.entries.len());
7070
}
7171
Err(e) => {
7272
println!("Failed to open regular repository: {:?}", e);
@@ -83,7 +83,7 @@ fn main() -> Result<()> {
8383

8484
// Note: status operations might behave differently on bare repos
8585
match opened_bare.status() {
86-
Ok(status) => println!(" Bare repository status: {} files", status.files.len()),
86+
Ok(status) => println!(" Bare repository status: {} files", status.entries.len()),
8787
Err(e) => println!(
8888
" Note: Status check on bare repo failed (expected): {:?}",
8989
e

0 commit comments

Comments
 (0)