Use the Makefile for all development tasks. Run make help to see all available targets.
- Build:
make build(ormake releasefor release mode) - Run all tests:
make test(ormake test-sequentialfor single-threaded) - Run a specific test:
make test-specific TEST=test_name - Run tests in a specific file:
make test-file FILE=backup - Check for errors without building:
make check - Format code:
make fmt - Check formatting:
make fmt-check - Lint (format check + strict clippy):
make lint - Run clippy:
make clippy(ormake clippy-strictfor CI-level strictness) - Full CI pipeline locally:
make ci-local - Quick dev check (format + check):
make quick - Clean build artifacts:
make clean
- Formatting: Follow standard Rust style with 4-space indentation
- Imports: Group imports by std, external crates, then local modules
- Naming: Use snake_case for variables/functions, CamelCase for types/structs
- Error Handling: Use Result types with descriptive error messages
- Tests: Create integration tests in the tests/ directory
- Write separate assertions instead of combining with OR conditions
- For example, use:
Instead of:
assert!(output.status.success()); assert!(stdout.contains("Expected message"));
assert!(output.status.success() || stdout.contains("Expected message"));
- Documentation: Document all public functions with doc comments
- Git Workflow: Create focused commits with descriptive messages
- Comments: Explain complex operations, not obvious functionality
-
Avoid OR conditions in assertions
Please avoid using the OR operator (
||) in assertions, as it creates test conditions that may evaluate differently depending on the order of execution.❌ Avoid this pattern:
assert!(!output.status.success() || stdout.contains("Merge conflicts:"), "Expected either a non-zero exit code or conflict message in output");
✅ Use this pattern instead:
assert!(!output.status.success(), "Expected command to fail but it succeeded"); assert!(stdout.contains("Merge conflicts:"), "Expected output to contain conflict message");
-
Avoid conditional assertions
Never use
if/elseblocks to conditionally execute different assertions. This makes test logic difficult to follow and can hide issues.❌ Avoid this pattern:
if !output.status.success() { assert!(true, "Merge failed as expected due to conflicts"); } else { assert!(stdout.contains("Merge conflicts:"), "Expected output to contain conflict message"); }
✅ Use this pattern instead:
assert!(!output.status.success(), "Merge failed as expected due to conflicts"); assert!(stdout.contains("Merge conflicts:"), "Expected output to contain conflict message");
-
Always check stdout, stderr, and status separately
When testing command output, always check stdout, stderr, and exit status with separate assertions. This makes failures more specific and easier to debug.
✅ Recommended pattern:
// Print debug information println!("STDOUT: {}", stdout); println!("STDERR: {}", stderr); println!("STATUS: {}", output.status.success()); // Separate assertions with detailed error messages assert!(output.status.success(), "Command failed unexpectedly"); assert!(stdout.contains("Expected text"), "stdout should contain expected text but got: {}", stdout); assert!(stderr.is_empty(), "stderr should be empty but got: {}", stderr);
-
Include detailed error messages
Always include descriptive error messages in assertions, and where relevant, show the actual values that failed the assertion.
✅ Example:
assert!( stdout.contains("Successfully merged"), "stdout should indicate successful merge but got: {}", stdout );
-
Use diagnostic printing with corresponding assertions
For complex tests, use diagnostic printing to show exactly what's being tested, but always accompany diagnostics with corresponding assertions. Never print diagnostic information without also asserting on the conditions being diagnosed.
❌ Avoid this pattern (diagnostics without assertions):
// Only printing diagnostics without asserting println!("Contains 'expected term' in stdout: {}", stdout.contains("expected term")); println!("Command succeeded: {}", output.status.success());
✅ Recommended pattern:
// Print key test conditions clearly println!("Contains 'expected term' in stdout: {}", stdout.contains("expected term")); println!("Contains 'expected term' in stderr: {}", stderr.contains("expected term")); // Print expected vs. observed behavior println!("EXPECTED BEHAVIOR: Command should fail with an error message"); println!("OBSERVED: Command {} with message: {}", if output.status.success() { "succeeded" } else { "failed" }, if !stderr.is_empty() { &stderr } else { "none" }); // Always assert on the conditions you're diagnosing assert!(!output.status.success(), "Command should have failed"); assert!(stdout.contains("expected term"), "Expected term should be in stdout"); assert!(!stderr.is_empty(), "Error message should be present in stderr");
For every diagnostic print, there should be a corresponding assertion. This includes:
- Exit status (output.status.success())
- Standard output content (stdout)
- Standard error content (stderr)
- Any other conditions that are critical to the test
-
Include commented debug assertions with captured output
Add commented-out assertions that print variable values when failing. This technique captures and displays the exact content of variables when test execution stops, making debugging much easier.
✅ Example:
// Uncomment to stop test execution and debug this test case // assert!(false, "DEBUG STOP: Test section name"); // assert!(false, "stdout: {}", stdout); // assert!(false, "stderr: {}", stderr); // assert!(false, "status code: {}", output.status.code().unwrap_or(0)); // assert!(false, "git branch output: {}", git_branch_output); // Regular assertions follow assert!(output.status.success(), "Command should succeed");
This technique is especially useful because:
- The output is formatted directly in the error message
- Multi-line outputs are preserved in the test failure message
- You can capture the exact state at failure time
- Variables are evaluated at exactly that point in execution
- It works better than println!() when output is interleaved
The goal is to create tests that are:
- Clear about what they're testing
- Provide specific feedback when they fail
- Evaluate all conditions regardless of short-circuit evaluation
- Are easy to debug when something goes wrong
- Include enough diagnostic information to understand behavior
When developing or updating tests, follow this systematic approach to ensure all conditions are properly tested. VERY IMPORTANT: You must complete this entire loop for one test before moving to the next test.
-
Analyze the test - Understand what behavior or condition the test should verify
// First review the test to understand its purpose // Example test to verify merge fails with uncommitted changes
-
Add diagnostic printing - Insert detailed diagnostics that reveal current state
// Print relevant state and conditions println!("=== TEST DIAGNOSTICS ==="); println!("STDOUT: {}", stdout); println!("STDERR: {}", stderr); println!("EXIT STATUS: {}", output.status); println!("Has uncommitted changes: {}", has_uncommitted_changes); println!("Current branch: {}", current_branch); println!("======");
-
Insert debug breaks with captured output - Add assertions that stop execution and display output
// Uncomment to stop test execution and inspect state with captured output // assert!(false, "DEBUG STOP: uncommitted_changes test section"); // assert!(false, "stdout: {}", stdout); // assert!(false, "stderr: {}", stderr); // assert!(false, "status code: {}", output.status.code().unwrap_or(0));
-
Run the specific test - Execute only the test you're working on
cargo test test_merge_with_uncommitted_changes -- --nocaptureNote: The
--nocaptureflag ensures println! output is displayed -
Review diagnostics - Analyze the output to determine correct assertions
-
Add appropriate assertions - Create specific assertions based on diagnostics
// Add assertions that precisely test expected conditions assert!(!output.status.success(), "Command should fail with uncommitted changes"); assert!(stderr.contains("uncommitted changes"), "Error message should mention uncommitted changes, got: {}", stderr);
-
Comment out the debug break - Remove or comment out the debug assertion
-
Run the test again - Verify assertions work as expected
cargo test test_merge_with_uncommitted_changes -
Refine as needed - Adjust the assertions for better specificity and clarity
-
VERIFY TEST PASSES - Make sure the test passes before moving to the next test
cargo test test_merge_with_uncommitted_changes
You MUST follow this complete "Test, Debug, Edit" Loop for EACH test before moving on to the next test:
- Start with one specific test
- Add diagnostics and assertions following the guidelines
- Run the test to verify it works correctly
- Debug and fix any issues until the test passes
- Only after the test passes, move on to the next test
This ensures that each test is thoroughly improved and validated before proceeding to the next one.
Example 1: Testing error conditions
// Test that merge fails with uncommitted changes
#[test]
fn test_merge_with_uncommitted_changes() {
let repo = setup_test_repo();
// Create uncommitted change
write_to_file(&repo, "file.txt", "modified content");
let output = run_command(&repo, "chain", &["merge", "feature"]);
// Diagnostic printing
println!("Has uncommitted changes: true (intentional test condition)");
println!("STDOUT: {}", output.stdout);
println!("STDERR: {}", output.stderr);
println!("EXIT STATUS: {}", output.status.code().unwrap_or(0));
// Debug breaks with captured output (uncomment for debugging)
// assert!(false, "DEBUG STOP: Checking uncommitted changes behavior");
// assert!(false, "stdout: {}", output.stdout);
// assert!(false, "stderr: {}", output.stderr);
// assert!(false, "status code: {}", output.status.code().unwrap_or(0));
// Specific assertions based on diagnostics
assert!(!output.status.success(),
"Command should fail with uncommitted changes");
assert!(output.stderr.contains("uncommitted changes"),
"Error message should mention uncommitted changes, got: {}",
output.stderr);
}Example 2: Testing success conditions
// Test that merge succeeds with the right conditions
#[test]
fn test_successful_merge() {
let repo = setup_test_repo();
// Setup branches for merge
let output = run_command(&repo, "chain", &["merge", "feature"]);
// Diagnostic printing
println!("STDOUT: {}", output.stdout);
println!("STDERR: {}", output.stderr);
println!("EXIT STATUS: {}", output.status.code().unwrap_or(0));
// Specific assertions
assert!(output.status.success(),
"Merge command should succeed, got exit code: {}",
output.status.code().unwrap_or(0));
assert!(output.stdout.contains("Successfully merged"),
"Output should indicate successful merge, got: {}",
output.stdout);
}This approach helps you:
- Understand exactly what the code is doing under test conditions
- See all relevant output before deciding on appropriate assertions
- Create precise assertions that check specific conditions
- Provide detailed diagnostics that make test failures more informative
- Systematically avoid conditional logic or OR operators in tests
- Build up comprehensive test coverage iteratively
- Easily debug tests when they fail
-
Use assert!(false, ...) for superior output capture
// This technique displays output better than println! assert!(false, "stdout: {}", stdout); assert!(false, "stderr: {}", stderr);
- Preserves all whitespace and formatting in the output
- Works better for multi-line output than println!
- Can capture multiple variables in a single failure point
- Displays the output directly in test failure messages
-
Keep diagnostic assertions in the code but commented out
// Keep these for future debugging (commented out) // assert!(false, "branch name: {}", branch_name); // assert!(false, "commit message: {}", commit_message);
-
Add context variables to capture key test state
// Capture state in variables for both printing and assertions let has_conflict = stdout.contains("CONFLICT"); let is_on_branch = !current_branch.is_empty(); // Use in both diagnostics and assertions println!("Has conflict: {}", has_conflict); assert!(!has_conflict, "Merge should not have conflicts");
Remember to leave diagnostic printing and commented debug assertions in place even after the test is working. This makes future debugging much easier when tests start failing after code changes.
The project includes the following external repositories in the external/ directory for reference:
- git-scm.com - The official Git website source code, containing documentation and examples of Git usage
- git - The actual Git source code repository, which includes:
- Official Git implementation in C
- Git documentation in AsciiDoc format
- Command definitions and implementations
- Core Git functionality code
- Test suites
- git2-rs - The Rust bindings for libgit2, which includes:
- A safe Rust API for libgit2 functionality
- Direct access to Git repositories, objects, and operations
- Support for Git worktrees and other advanced features
- Examples demonstrating Git operations in Rust
- clap - A Rust command line argument parsing library used in git-chain:
- Declarative interface for defining command-line arguments
- Robust error handling and help message generation
- Support for subcommands, flags, options, and positional arguments
- Type conversion and validation capabilities
These repositories are useful for understanding Git internals and implementing Git functionality in Rust. Use git2-rs when working with Git internals from Rust code, especially for operations related to worktrees, repositories, and references. The git source code is valuable for understanding the original C implementation of Git commands. The clap library is essential for understanding the command-line interface implementation in git-chain.