First off, thank you for considering contributing to cgen! Every contribution helps — whether it's a bug report, a new provider, documentation improvement, or a feature implementation.
- Getting Started
- Development Setup
- Project Structure
- Adding a New Default Provider
- Making Changes
- Pull Request Process
- Code Style
- Reporting Bugs
- Suggesting Features
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/smart-commit-rs.git cd smart-commit-rs - Add the upstream remote:
git remote add upstream https://github.com/gtkacz/smart-commit-rs.git
- Rust (stable toolchain, 1.85+)
- Git
# Debug build (fast compilation, slower binary)
cargo build
# Run directly
cargo run
# Run with arguments
cargo run -- config
# Release build (slow compilation, optimized binary)
cargo build --release
# Run tests
cargo test
# Check for warnings without building
cargo check
# Format code
cargo fmt
# Lint
cargo clippyTo test the full flow you need a valid API key for at least one provider:
# Set a key for testing
export ACR_API_KEY=your-test-key
# Stage some changes and run
git add some_file
cargo runTo test the interactive config menu:
cargo run -- config # Inside a repo: choose local (.env) or global (TOML)
cargo run -- config # Outside a repo: opens global TOML directlyTo test new commit workflow controls:
# Dry run (should NOT create a commit)
cargo run -- --dry-run
# Undo latest commit (soft reset, keeps changes staged)
cargo run -- undoSuggested manual smoke checks:
ACR_POST_COMMIT_PUSH=never|ask|alwaysbehavior after commit creationACR_WARN_STAGED_FILES_ENABLED=1+ lowACR_WARN_STAGED_FILES_THRESHOLDto trigger confirmationACR_SUPPRESS_TOOL_OUTPUT=1to confirm git output is hiddenundoon unpushed commitundoon pushed commit (should warn and require confirmation)
Automated quality gates (same checks as CI):
# Full test suite
cargo test --locked
# Coverage gate for core logic
cargo llvm-cov --locked --lib --tests \
--ignore-filename-regex '(main|cli|preset|update|cache|ui)\.rs' \
--summary-only \
--fail-under-lines 90
# Required formatting and lint gates
cargo fmt --all --check
cargo clippy --locked --all-targets --all-features -- -D warningssrc/
├── main.rs # Entry point, CLI dispatch, main flow
├── cli.rs # clap derive definitions + interactive config menu (inquire)
├── config.rs # AppConfig struct, layered resolution, TOML/env I/O
├── persistence.rs # Locked, owner-only atomic persistence
├── provider.rs # Provider registry, API adapters, HTTP call, response parsing
├── prompt.rs # System prompt assembly and final-message validation
├── git.rs # Git operations, path-aware filtering, diff inspection
├── interpolation.rs # Non-mutating $VAR template engine for URL/headers
├── editor.rs # Shell-free external editor launcher
├── preset.rs # Presets and fallback configuration
├── cache.rs # Bounded per-repository generated-commit history
├── update.rs # Provenance-aware, checksum-verified updater
└── workflow.rs # Testable pre-provider workflow policy
.github/workflows/
├── test.yml # Cross-platform test, quality, MSRV, audit, coverage
└── release.yml # Locked multi-platform release builds and checksums
Design principles:
- One file = one concern. No nested module directories.
- Synchronous only — no async runtime (
ureqinstead ofreqwest+tokio). - Minimal dependencies — every crate must justify its inclusion by binary size or maintenance burden.
std::process::Commandfor git operations — nogit2/gitoxidefor 3 shell commands.
This is one of the easiest and most valuable ways to contribute. A default provider means users can just set ACR_PROVIDER=provider_name and ACR_API_KEY=... without needing to configure the URL or headers manually.
-
Open
src/provider.rsand find theget_provider()function. -
Add a new match arm with the provider's API details:
"your_provider" => Some(ProviderDef { api_url: "https://api.example.com/v1/chat/completions", api_headers: "Authorization: Bearer $ACR_API_KEY", default_model: "your-model", format: RequestFormat::OpenAiCompat, // or Gemini, Anthropic, LmStudio response_path: "choices.0.message.content", }),
-
Choose the right
RequestFormat:OpenAiCompat— Most providers use this (OpenAI-compatible chat completions). Request body:{ model, messages: [{role, content}], max_tokens, temperature }.Gemini— Google's format withsystem_instructionandcontentsarrays.Anthropic— Similar to OpenAI but withsystemas a top-level string field.LmStudio— LM Studio chat endpoint format. Request body:{ model, system_prompt, input }.
If the provider uses a completely different format, you may need to add a new variant to
RequestFormatand a matching arm inbuild_request_body(). -
Set
response_path— this is a dot-separated path to the generated text in the JSON response. For example:- OpenAI-compatible:
choices.0.message.content - Gemini:
candidates.0.content.parts.0.text - LM Studio:
output(the parser selects the item wheretype == "message"and returns itscontent) - Use numbers for array indices:
results.0.text
- OpenAI-compatible:
-
URL/header interpolation — you can use
$ACR_API_KEY,$ACR_MODEL, or any environment variable in theapi_urlandapi_headersstrings. They get expanded at runtime. -
Update
src/cli.rs— add the provider name to thechoiceslist in the"PROVIDER"match arm ofinteractive_config():"PROVIDER" => { let choices = vec!["gemini", "openai", "anthropic", "your_provider", "(custom)"]; // ... }
-
Update the README — add the provider to the "Built-in providers" line in the Providers section.
-
Test it — if you have access to the provider's API, verify the full flow works. If not, mention this in your PR and someone will test it before merging.
"mistral" => Some(ProviderDef {
api_url: "https://api.mistral.ai/v1/chat/completions",
api_headers: "Authorization: Bearer $ACR_API_KEY",
default_model: "mistral-small-latest",
format: RequestFormat::OpenAiCompat,
response_path: "choices.0.message.content",
}),"lm_studio" => Some(ProviderDef {
api_url: "http://localhost:1234/api/v1/chat",
api_headers: "Content-Type: application/json",
default_model: "qwen/qwen3.5-35b-a3b",
format: RequestFormat::LmStudio,
response_path: "output",
}),That's it — most OpenAI-compatible providers are short additions, while custom payload APIs (like LM Studio) need a dedicated request/response format branch.
-
Create a feature branch from
main:git checkout main git pull upstream main git checkout -b feature/your-feature-name
-
Make your changes — keep commits focused and atomic.
-
Ensure quality:
cargo fmt --all --check cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked cargo build --locked -
Commit with a descriptive message following Conventional Commits or just use
cgen;) :feat(provider): add Mistral as default provider fix(config): handle missing .env gracefully docs: add Mistral to provider list in README
-
Push your branch to your fork:
git push origin feature/your-feature-name
-
Open a Pull Request against
mainon the upstream repository. -
In the PR description, include:
- What the change does and why
- How to test it (if applicable)
- Screenshots for UI changes (config menu, spinner, etc.)
-
Keep it small — one concern per PR. A provider addition + a bug fix should be two separate PRs.
-
Be responsive — if changes are requested, push follow-up commits to the same branch.
- Follows the existing code patterns and style
- Doesn't introduce new dependencies without justification
- Keeps the binary small (check
cargo build --releasesize) - Includes documentation updates when behavior changes
- Has a clear, concise title using conventional commit format
- Format: Always run
cargo fmtbefore committing. - Lints: Fix all
cargo clippywarnings. - Error handling: Use
anyhowfor errors. Use.context("description")to add context to errors. Usebail!()for early returns. - No
unwrap()in production code — use?or.context()instead.unwrap()is acceptable only in cases where failure is truly impossible (e.g., compiling a hardcoded regex). - Dependencies: Prefer crates that are lightweight and well-maintained. Always consider binary size impact. If a feature can be done in 20 lines of code, don't add a crate for it.
- Comments: Only where the "why" isn't obvious from the code. No doc comments on private internals unless they're complex.
Open an issue with:
- cgen version (
cgen --version) - OS and architecture (e.g., Windows 11 x64, macOS ARM)
- What you expected vs what happened
- Steps to reproduce — the minimum commands to trigger the bug
- Error output — full terminal output including the error message
Open an issue with:
- What problem it solves — describe the use case, not just the solution
- Proposed behavior — how it would work from the user's perspective
- Alternatives considered — other ways you thought about solving it
By contributing, you agree that your contributions will be licensed under the MIT License.