Skip to content

Commit 1c2d5ca

Browse files
committed
Fix tests
1 parent 47f8aa1 commit 1c2d5ca

File tree

12 files changed

+506
-160
lines changed

12 files changed

+506
-160
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"Bash(find:*)",
1616
"Bash(ls:*)",
1717
"Bash(timeout 60 cargo tarpaulin --all-features --workspace --exclude-files=tests/* --timeout 30)",
18-
"Bash(pkill:*)"
18+
"Bash(pkill:*)",
19+
"Bash(rg:*)"
1920
],
2021
"deny": []
2122
}

CLAUDE.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1414
- `make test` - Run unit and integration tests
1515
- `cargo test` - Alternative test command
1616
- `cargo test <test_name>` - Run specific test
17+
- `make coverage` - Generate test coverage using tarpaulin
1718

1819
### Code Quality
1920
- `make fmt` - Format code with rustfmt
@@ -41,11 +42,16 @@ Each Git workflow command is implemented as a separate module:
4142
- Error handling uses `expect()` with descriptive messages
4243

4344
### Key Dependencies
44-
- `clap` - Command-line parsing with derive macros
45-
- `clap_complete` - Shell completion generation
46-
- `console` - Terminal output formatting
45+
- `clap` - Command-line parsing with derive macros and subcommands
46+
- `console` - Terminal output formatting with colors and emojis
4747
- `chrono` - Date/time handling for summary command
4848

49+
### Dev Dependencies
50+
- `assert_cmd` - CLI testing framework for integration tests
51+
- `tempfile` - Temporary Git repositories for testing
52+
- `predicates` - Assertion helpers for test conditions
53+
- `criterion` - Benchmarking framework
54+
4955
### Testing
5056
- Integration tests in `tests/` directory
5157
- Each command has corresponding `test_<command>.rs` file
@@ -54,5 +60,10 @@ Each Git workflow command is implemented as a separate module:
5460
### Git Plugin Integration
5561
The binary name `git-x` enables Git's plugin system - any executable named `git-<name>` can be invoked as `git <name>`. Commands like `git x info` work automatically once installed.
5662

57-
### Completion System
58-
Shell completions are generated via `--generate-completions <shell>` flag using `clap_complete`. The README provides setup instructions for bash, zsh, fish, PowerShell, and Elvish.
63+
### Error Handling and Types
64+
- Common error type `GitXError` defined in `src/lib.rs` with variants for Git commands, IO, and parsing
65+
- Most commands use `expect()` with descriptive messages for quick failure feedback
66+
- Type alias `Result<T>` available for consistent error handling
67+
68+
### Shell Completion Generation
69+
Shell completions can be generated via `--generate-completions <shell>` flag using `clap_complete`. The README provides setup instructions for bash, zsh, fish, PowerShell, and Elvish.

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ run: build
2323
test:
2424
$(CARGO) test
2525

26+
## Run tests serially (useful for debugging test interference)
27+
test-serial:
28+
$(CARGO) test -- --test-threads=1
29+
2630
## Run test coverage analysis using tarpaulin
2731
coverage:
2832
$(CARGO) tarpaulin --workspace --timeout 120 --out Stdout
@@ -67,6 +71,7 @@ help:
6771
@echo " make build Build release binary"
6872
@echo " make run Run binary with ARGS=\"xinfo\""
6973
@echo " make test Run tests"
74+
@echo " make test-serial Run tests serially (for debugging)"
7075
@echo " make coverage Generate test coverage report"
7176
@echo " make fmt Format code"
7277
@echo " make fmt-check Check formatting"

src/health.rs

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
1+
use crate::{GitXError, Result};
12
use console::Style;
23
use std::process::Command;
34

4-
pub fn run() {
5+
pub fn run() -> Result<String> {
56
let bold = Style::new().bold();
67
let green = Style::new().green().bold();
78
let yellow = Style::new().yellow().bold();
89
let red = Style::new().red().bold();
910

10-
println!("{}", bold.apply_to("Repository Health Check"));
11-
println!("{}", bold.apply_to("========================="));
12-
println!();
11+
let mut output = Vec::new();
12+
13+
output.push(format!("{}", bold.apply_to("Repository Health Check")));
14+
output.push(format!("{}", bold.apply_to("=========================")));
15+
output.push(String::new());
1316

1417
// Check if we're in a git repository
1518
if !is_git_repo(&std::env::current_dir().unwrap_or_else(|_| ".".into())) {
16-
println!("{} Not in a Git repository", red.apply_to("✗"));
17-
return;
19+
output.push(format!("{} Not in a Git repository", red.apply_to("✗")));
20+
return Ok(output.join("\n"));
1821
}
1922

2023
// 1. Check repository status
21-
check_repo_status(&green, &yellow, &red);
24+
output.push(check_repo_status(&green, &yellow, &red)?);
2225

2326
// 2. Check for untracked files
24-
check_untracked_files(&green, &yellow, &red);
27+
output.push(check_untracked_files(&green, &yellow, &red)?);
2528

2629
// 3. Check for stale branches
27-
check_stale_branches(&green, &yellow, &red);
30+
output.push(check_stale_branches(&green, &yellow, &red)?);
2831

2932
// 4. Check repository size
30-
check_repo_size(&green, &yellow, &red);
33+
output.push(check_repo_size(&green, &yellow, &red)?);
3134

3235
// 5. Check for uncommitted changes
33-
check_uncommitted_changes(&green, &yellow, &red);
36+
output.push(check_uncommitted_changes(&green, &yellow, &red)?);
37+
38+
output.push(String::new());
39+
output.push(format!("{}", bold.apply_to("Health check complete!")));
3440

35-
println!();
36-
println!("{}", bold.apply_to("Health check complete!"));
41+
Ok(output.join("\n"))
3742
}
3843

3944
pub fn is_git_repo(path: &std::path::Path) -> bool {
@@ -45,50 +50,74 @@ pub fn is_git_repo(path: &std::path::Path) -> bool {
4550
.unwrap_or(false)
4651
}
4752

48-
fn check_repo_status(green: &Style, _yellow: &Style, red: &Style) {
53+
fn check_repo_status(green: &Style, _yellow: &Style, red: &Style) -> Result<String> {
4954
let output = Command::new("git")
5055
.args(["status", "--porcelain"])
5156
.output()
52-
.expect("Failed to run git status");
57+
.map_err(|_| GitXError::GitCommand("Failed to run git status".to_string()))?;
58+
59+
if !output.status.success() {
60+
return Err(GitXError::GitCommand(
61+
"Failed to get repository status".to_string(),
62+
));
63+
}
5364

5465
let status_output = String::from_utf8_lossy(&output.stdout);
5566

5667
if status_output.trim().is_empty() {
57-
println!("{} Working directory is clean", green.apply_to("✓"));
68+
Ok(format!(
69+
"{} Working directory is clean",
70+
green.apply_to("✓")
71+
))
5872
} else {
59-
println!("{} Working directory has changes", red.apply_to("✗"));
73+
Ok(format!(
74+
"{} Working directory has changes",
75+
red.apply_to("✗")
76+
))
6077
}
6178
}
6279

63-
fn check_untracked_files(green: &Style, yellow: &Style, _red: &Style) {
80+
fn check_untracked_files(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
6481
let output = Command::new("git")
6582
.args(["ls-files", "--others", "--exclude-standard"])
6683
.output()
67-
.expect("Failed to list untracked files");
84+
.map_err(|_| GitXError::GitCommand("Failed to list untracked files".to_string()))?;
85+
86+
if !output.status.success() {
87+
return Err(GitXError::GitCommand(
88+
"Failed to get untracked files".to_string(),
89+
));
90+
}
6891

6992
let untracked = String::from_utf8_lossy(&output.stdout);
7093
let untracked_files: Vec<&str> = untracked.lines().collect();
7194

7295
if untracked_files.is_empty() {
73-
println!("{} No untracked files", green.apply_to("✓"));
96+
Ok(format!("{} No untracked files", green.apply_to("✓")))
7497
} else {
75-
println!(
98+
Ok(format!(
7699
"{} {} untracked files found",
77100
yellow.apply_to("!"),
78101
untracked_files.len()
79-
);
102+
))
80103
}
81104
}
82105

83-
fn check_stale_branches(green: &Style, yellow: &Style, _red: &Style) {
106+
fn check_stale_branches(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
84107
let output = Command::new("git")
85108
.args([
86109
"for-each-ref",
87110
"--format=%(refname:short) %(committerdate:relative)",
88111
"refs/heads/",
89112
])
90113
.output()
91-
.expect("Failed to list branches");
114+
.map_err(|_| GitXError::GitCommand("Failed to list branches".to_string()))?;
115+
116+
if !output.status.success() {
117+
return Err(GitXError::GitCommand(
118+
"Failed to get branch information".to_string(),
119+
));
120+
}
92121

93122
let branches = String::from_utf8_lossy(&output.stdout);
94123
let mut stale_count = 0;
@@ -100,24 +129,30 @@ fn check_stale_branches(green: &Style, yellow: &Style, _red: &Style) {
100129
}
101130

102131
if stale_count == 0 {
103-
println!(
132+
Ok(format!(
104133
"{} No stale branches (older than 1 month)",
105134
green.apply_to("✓")
106-
);
135+
))
107136
} else {
108-
println!(
137+
Ok(format!(
109138
"{} {} potentially stale branches found",
110139
yellow.apply_to("!"),
111140
stale_count
112-
);
141+
))
113142
}
114143
}
115144

116-
fn check_repo_size(green: &Style, yellow: &Style, red: &Style) {
145+
fn check_repo_size(green: &Style, yellow: &Style, red: &Style) -> Result<String> {
117146
let output = Command::new("du")
118147
.args(["-sh", ".git"])
119148
.output()
120-
.expect("Failed to check repository size");
149+
.map_err(|_| GitXError::GitCommand("Failed to check repository size".to_string()))?;
150+
151+
if !output.status.success() {
152+
return Err(GitXError::GitCommand(
153+
"Failed to get repository size".to_string(),
154+
));
155+
}
121156

122157
let size_output = String::from_utf8_lossy(&output.stdout);
123158
let size = size_output.split_whitespace().next().unwrap_or("unknown");
@@ -126,44 +161,50 @@ fn check_repo_size(green: &Style, yellow: &Style, red: &Style) {
126161
if size.ends_with('K')
127162
|| (size.ends_with('M') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 5)
128163
{
129-
println!(
164+
Ok(format!(
130165
"{} Repository size: {} (healthy)",
131166
green.apply_to("✓"),
132167
size
133-
);
168+
))
134169
} else if size.ends_with('M')
135170
|| (size.ends_with('G') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 1)
136171
{
137-
println!(
172+
Ok(format!(
138173
"{} Repository size: {} (moderate)",
139174
yellow.apply_to("!"),
140175
size
141-
);
176+
))
142177
} else {
143-
println!(
178+
Ok(format!(
144179
"{} Repository size: {} (large - consider cleanup)",
145180
red.apply_to("✗"),
146181
size
147-
);
182+
))
148183
}
149184
}
150185

151-
fn check_uncommitted_changes(green: &Style, yellow: &Style, _red: &Style) {
186+
fn check_uncommitted_changes(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
152187
let output = Command::new("git")
153188
.args(["diff", "--cached", "--name-only"])
154189
.output()
155-
.expect("Failed to check staged changes");
190+
.map_err(|_| GitXError::GitCommand("Failed to check staged changes".to_string()))?;
191+
192+
if !output.status.success() {
193+
return Err(GitXError::GitCommand(
194+
"Failed to get staged changes".to_string(),
195+
));
196+
}
156197

157198
let staged = String::from_utf8_lossy(&output.stdout);
158199
let staged_files: Vec<&str> = staged.lines().filter(|line| !line.is_empty()).collect();
159200

160201
if staged_files.is_empty() {
161-
println!("{} No staged changes", green.apply_to("✓"));
202+
Ok(format!("{} No staged changes", green.apply_to("✓")))
162203
} else {
163-
println!(
204+
Ok(format!(
164205
"{} {} files staged for commit",
165206
yellow.apply_to("!"),
166207
staged_files.len()
167-
);
208+
))
168209
}
169210
}

src/main.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ fn main() {
1616
Commands::Info => info::run(),
1717
Commands::Graph => graph::run(),
1818
Commands::ColorGraph => color_graph::run(),
19-
Commands::Health => health::run(),
19+
Commands::Health => match health::run() {
20+
Ok(output) => println!("{output}"),
21+
Err(e) => eprintln!("Error: {e}"),
22+
},
2023
Commands::Since { reference } => since::run(reference),
2124
Commands::Undo => undo::run(),
2225
Commands::CleanBranches { dry_run } => clean_branches::run(dry_run),
23-
Commands::What { target } => what::run(target),
26+
Commands::What { target } => match what::run(target) {
27+
Ok(output) => println!("{output}"),
28+
Err(e) => eprintln!("Error: {e}"),
29+
},
2430
Commands::Summary { since } => summary::run(since),
2531
Commands::Sync { merge } => sync::run(merge),
26-
Commands::New { branch_name, from } => new_branch::run(branch_name, from),
32+
Commands::New { branch_name, from } => match new_branch::run(branch_name, from) {
33+
Ok(output) => println!("{output}"),
34+
Err(e) => eprintln!("Error: {e}"),
35+
},
2736
Commands::LargeFiles { limit, threshold } => large_files::run(limit, threshold),
2837
Commands::Fixup {
2938
commit_hash,

0 commit comments

Comments
 (0)