mirror of
https://github.com/prdlk/gh-commit.git
synced 2026-09-16 23:16:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3086dac5d6 | ||
|
|
f1302fd1ec | ||
|
|
ae36a7052d | ||
|
|
9647c854c9 |
@@ -1,2 +0,0 @@
|
|||||||
/.omc
|
|
||||||
/__pycache__
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Prad
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
# gh-commit
|
# gh-commit
|
||||||
|
|
||||||
AI-powered scoped git commits. Groups changes by project area, generates commit messages with `crush run`, and pushes — all in one command.
|
AI-powered scoped git commits as a GitHub CLI extension, driven by the
|
||||||
|
[Crush](https://github.com/charmbracelet/crush) CLI. Scopes map Conventional
|
||||||
|
Commit scope names to path prefixes. They are generated from the repository
|
||||||
|
file tree, stored in a local DuckDB database, and auto-regenerate whenever
|
||||||
|
`.gitignore` changes. Changed files are grouped by scope and committed one
|
||||||
|
scope at a time with generated `type(scope): message` subjects.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -11,64 +16,33 @@ gh extension install prdlk/gh-commit
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [uv](https://docs.astral.sh/uv) — Python package runner (handles dependencies automatically)
|
- [uv](https://docs.astral.sh/uv) — Python package runner (handles dependencies automatically)
|
||||||
- [Crush](https://github.com/charmbracelet/crush) — non-interactive AI runner
|
- [Crush](https://github.com/charmbracelet/crush) — configured with an authenticated model provider
|
||||||
- `GROQ_API_KEY` — authenticates the default Groq model
|
|
||||||
|
|
||||||
By default, gh-commit uses Groq's `openai/gpt-oss-120b`. Override the model with `GH_COMMIT_CRUSH_MODEL`.
|
By default, gh-commit uses `openrouter/qwen/qwen3.6-27b`. Configure OpenRouter in Crush or override the model with `GH_COMMIT_CRUSH_MODEL`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Initialize scopes for your repo (uses Crush to analyze structure)
|
cd your-repo
|
||||||
gh commit init
|
gh commit init # generate scopes for this repo
|
||||||
|
gh commit # commit changes grouped by scope
|
||||||
# Commit changes grouped by scope
|
|
||||||
gh commit
|
|
||||||
|
|
||||||
# Auto-confirm + auto-push
|
|
||||||
gh commit --auto --push
|
|
||||||
|
|
||||||
# Manually refresh scopes after structural changes
|
|
||||||
gh commit refresh
|
|
||||||
|
|
||||||
# Sync scopes as GitHub labels
|
|
||||||
gh commit sync
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
1. **`gh commit init`** — `crush run` analyzes your repo structure and generates scope definitions (e.g., `core → src/`, `docs → docs/, README.md`, `ci → .github/workflows/`)
|
|
||||||
2. **`gh commit`** — Groups dirty files by scope, sends each staged diff to `crush run`, and commits each group separately
|
|
||||||
3. **Auto-refresh** — Whenever your `.gitignore` changes, scopes are automatically regenerated before committing (a content hash of `.gitignore` is tracked per repo)
|
|
||||||
4. Remaining unscoped files are handled in a final pass
|
|
||||||
5. Unpushed commits are offered for push
|
|
||||||
|
|
||||||
Scopes are stored in a local DuckDB database (`~/.local/share/gh-commit/gh-commit.db`) — no config files in your repo.
|
|
||||||
|
|
||||||
### Why Crush?
|
|
||||||
|
|
||||||
The former Mods roles now live directly in `smartcommit.py`, so gh-commit no longer depends on Mods configuration. Each generation invokes Crush's supported non-interactive mode with a self-contained prompt.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Behavior |
|
||||||
|---------|-------------|
|
|---|---|
|
||||||
| `gh commit` | Commit changes grouped by scope |
|
| `gh commit` | Full scoped-commit flow: group changed files by scope, generate a message per group, confirm, commit, offer to push |
|
||||||
| `gh commit init` | Generate scopes for current repo |
|
| `gh commit init` | Generate scopes from the file tree via Crush; migrates legacy configs; confirms overwrite |
|
||||||
| `gh commit refresh` | Update scopes from current structure |
|
| `gh commit refresh` | Show current scopes, regenerate with existing scopes as context, confirm apply |
|
||||||
| `gh commit sync` | Sync scopes → GitHub labels |
|
| `gh commit sync` | Create/update GitHub labels from scopes (color = first 6 hex chars of MD5 of the scope name) |
|
||||||
| `gh commit list` | List all configured repositories |
|
| `gh commit list` | Table of configured repositories: name, path, scope count |
|
||||||
| `gh commit remove` | Remove current repo from database |
|
| `gh commit remove` | Delete the current repo from the database after confirmation |
|
||||||
| `gh commit db-path` | Print database file path |
|
| `gh commit db-path` | Print the database file path |
|
||||||
| `gh commit version` | Print version |
|
| `gh commit version` | Print `gh-commit <version>` |
|
||||||
| `gh commit help` | Show help |
|
| `gh commit help` | Help, including the DB path and environment variable docs |
|
||||||
|
|
||||||
## Flags
|
Flags: `--auto` (skip confirmations), `--push` (auto-push).
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `--auto` | Skip all confirmation prompts |
|
|
||||||
| `--push` | Auto-push after committing |
|
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
@@ -78,14 +52,39 @@ The former Mods roles now live directly in `smartcommit.py`, so gh-commit no lon
|
|||||||
| `GH_COMMIT_PUSH=1` | Auto-push after commits |
|
| `GH_COMMIT_PUSH=1` | Auto-push after commits |
|
||||||
| `GH_COMMIT_NO_AUTO_REFRESH=1` | Don't auto-regenerate scopes when `.gitignore` changes |
|
| `GH_COMMIT_NO_AUTO_REFRESH=1` | Don't auto-regenerate scopes when `.gitignore` changes |
|
||||||
| `GH_COMMIT_CRUSH_CMD` | Override the Crush command (default `crush`) |
|
| `GH_COMMIT_CRUSH_CMD` | Override the Crush command (default `crush`) |
|
||||||
| `GH_COMMIT_CRUSH_MODEL` | Override the Crush model (default `groq/openai/gpt-oss-120b`) |
|
| `GH_COMMIT_CRUSH_MODEL` | Override the Crush model (default `openrouter/qwen/qwen3.6-27b`) |
|
||||||
| `GH_COMMIT_CRUSH_TIMEOUT` | Per-prompt timeout in seconds (default `120`) |
|
| `GH_COMMIT_CRUSH_TIMEOUT` | Per-prompt timeout in seconds (default `120`) |
|
||||||
| `GH_COMMIT_DEBUG=1` | Show scope-response parse diagnostics |
|
| `GH_COMMIT_DEBUG=1` | Show scope-response parse diagnostics |
|
||||||
|
|
||||||
## Migration
|
## Storage
|
||||||
|
|
||||||
Existing `.github/Repo.toml` or `.github/scopes.json` files are automatically detected and migrated to DuckDB on first run.
|
Scopes live in `${XDG_DATA_HOME:-~/.local/share}/gh-commit/gh-commit.db`
|
||||||
|
(DuckDB). Legacy `.github/Repo.toml` and `.github/scopes.json` files are
|
||||||
|
migrated automatically on first run (the source file is archived as
|
||||||
|
`*.migrated.<timestamp>`).
|
||||||
|
|
||||||
## License
|
## Manual acceptance
|
||||||
|
|
||||||
MIT
|
```sh
|
||||||
|
mkdir /tmp/accept && cd /tmp/accept && git init
|
||||||
|
mkdir -p src docs
|
||||||
|
echo 'package app' > src/app.go
|
||||||
|
echo '# Docs' > docs/README.md
|
||||||
|
gh commit init # scopes generated and saved
|
||||||
|
echo '// change' >> src/app.go
|
||||||
|
echo 'More docs' >> docs/README.md
|
||||||
|
gh commit # one commit per scope
|
||||||
|
git log --format='%s' # verify type(scope): message subjects
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The extension is a bash launcher (`gh-commit`) that runs `smartcommit.py`
|
||||||
|
with `uv run --script`; uv resolves the inline dependencies (duckdb, rich,
|
||||||
|
questionary) automatically.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -m py_compile smartcommit.py # syntax check
|
||||||
|
bash -n gh-commit # launcher check
|
||||||
|
./gh-commit version # smoke test
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://charm.land/crush.json",
|
|
||||||
"providers": {
|
|
||||||
"groq": {
|
|
||||||
"type": "openai-compat",
|
|
||||||
"base_url": "https://api.groq.com/openai/v1",
|
|
||||||
"api_key": "$GROQ_API_KEY",
|
|
||||||
"models": [
|
|
||||||
{
|
|
||||||
"id": "openai/gpt-oss-120b",
|
|
||||||
"name": "GPT-OSS 120B on Groq",
|
|
||||||
"context_window": 131072,
|
|
||||||
"default_max_tokens": 8192
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# gh-commit launcher — runs the bundled Python implementation via uv.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# gh-commit: AI-powered scoped git commits (Crush for scopes and messages)
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# Install: gh extension install prdlk/gh-commit
|
|
||||||
# Requires: uv (https://docs.astral.sh/uv) and the Crush CLI
|
|
||||||
# (https://github.com/charmbracelet/crush)
|
|
||||||
|
|
||||||
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"
|
if ! command -v uv >/dev/null 2>&1; then
|
||||||
SCRIPT="${EXTENSION_DIR}/smartcommit.py"
|
echo "gh-commit: 'uv' is required (https://docs.astral.sh/uv)" >&2
|
||||||
|
|
||||||
if ! command -v uv &>/dev/null; then
|
|
||||||
echo "error: 'uv' is required but not installed"
|
|
||||||
echo "install: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "${GH_COMMIT_CRUSH_CMD:-}" ]] && ! command -v crush &>/dev/null; then
|
exec uv run --script "$DIR/smartcommit.py" "$@"
|
||||||
echo "warning: 'crush' not found — gh-commit uses 'crush run' for scopes and commit messages."
|
|
||||||
echo "install Crush (https://github.com/charmbracelet/crush), or set GH_COMMIT_CRUSH_CMD."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec uv run --script "${SCRIPT}" "$@"
|
|
||||||
|
|||||||
Binary file not shown.
+4
-9
@@ -44,9 +44,7 @@ DB_PATH = DB_DIR / "gh-commit.db"
|
|||||||
# Crush handles both AI steps in non-interactive mode. The instructions formerly
|
# Crush handles both AI steps in non-interactive mode. The instructions formerly
|
||||||
# stored as mods roles are embedded below so this tool is self-contained.
|
# stored as mods roles are embedded below so this tool is self-contained.
|
||||||
CRUSH_CMD = os.environ.get("GH_COMMIT_CRUSH_CMD", "crush").split()
|
CRUSH_CMD = os.environ.get("GH_COMMIT_CRUSH_CMD", "crush").split()
|
||||||
DEFAULT_CRUSH_MODEL = "groq/openai/gpt-oss-120b"
|
CRUSH_MODEL = os.environ.get("GH_COMMIT_CRUSH_MODEL", "openrouter/qwen/qwen3.6-27b")
|
||||||
CRUSH_MODEL = os.environ.get("GH_COMMIT_CRUSH_MODEL", DEFAULT_CRUSH_MODEL)
|
|
||||||
CRUSH_CONFIG_DIR = Path(__file__).with_name("crush-provider")
|
|
||||||
CRUSH_TIMEOUT = int(os.environ.get("GH_COMMIT_CRUSH_TIMEOUT", "120"))
|
CRUSH_TIMEOUT = int(os.environ.get("GH_COMMIT_CRUSH_TIMEOUT", "120"))
|
||||||
|
|
||||||
AUTO_CONFIRM = os.environ.get("GH_COMMIT_AUTO", "0") == "1"
|
AUTO_CONFIRM = os.environ.get("GH_COMMIT_AUTO", "0") == "1"
|
||||||
@@ -54,7 +52,7 @@ AUTO_PUSH = os.environ.get("GH_COMMIT_PUSH", "0") == "1"
|
|||||||
NO_AUTO_REFRESH = os.environ.get("GH_COMMIT_NO_AUTO_REFRESH", "0") == "1"
|
NO_AUTO_REFRESH = os.environ.get("GH_COMMIT_NO_AUTO_REFRESH", "0") == "1"
|
||||||
DEBUG = os.environ.get("GH_COMMIT_DEBUG", "0") == "1"
|
DEBUG = os.environ.get("GH_COMMIT_DEBUG", "0") == "1"
|
||||||
|
|
||||||
VERSION = "2.0.0"
|
VERSION = "5.0.0"
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
@@ -117,12 +115,9 @@ def crush_prompt(text: str, cwd: Path) -> str:
|
|||||||
cmd = [*CRUSH_CMD, "run", "--quiet"]
|
cmd = [*CRUSH_CMD, "run", "--quiet"]
|
||||||
if CRUSH_MODEL:
|
if CRUSH_MODEL:
|
||||||
cmd += ["--model", CRUSH_MODEL]
|
cmd += ["--model", CRUSH_MODEL]
|
||||||
env = os.environ.copy()
|
|
||||||
if CRUSH_MODEL == DEFAULT_CRUSH_MODEL:
|
|
||||||
env.setdefault("CRUSH_GLOBAL_CONFIG", str(CRUSH_CONFIG_DIR))
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd, input=text, capture_output=True, text=True, env=env,
|
cmd, input=text, capture_output=True, text=True,
|
||||||
cwd=str(cwd), timeout=CRUSH_TIMEOUT,
|
cwd=str(cwd), timeout=CRUSH_TIMEOUT,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
@@ -677,7 +672,7 @@ def cmd_help():
|
|||||||
console.print(" GH_COMMIT_PUSH=1 Auto-push after commits")
|
console.print(" GH_COMMIT_PUSH=1 Auto-push after commits")
|
||||||
console.print(" GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change")
|
console.print(" GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change")
|
||||||
console.print(" GH_COMMIT_CRUSH_CMD=... Override the Crush command")
|
console.print(" GH_COMMIT_CRUSH_CMD=... Override the Crush command")
|
||||||
console.print(f" GH_COMMIT_CRUSH_MODEL=... Override the Crush model (default: {DEFAULT_CRUSH_MODEL})")
|
console.print(" GH_COMMIT_CRUSH_MODEL=... Override the Crush model (default: openrouter/qwen/qwen3.6-27b)")
|
||||||
console.print(" GH_COMMIT_CRUSH_TIMEOUT=... Per-prompt timeout in seconds (default: 120)")
|
console.print(" GH_COMMIT_CRUSH_TIMEOUT=... Per-prompt timeout in seconds (default: 120)")
|
||||||
console.print(" GH_COMMIT_DEBUG=1 Show parse diagnostics\n")
|
console.print(" GH_COMMIT_DEBUG=1 Show parse diagnostics\n")
|
||||||
console.print("[cyan]Scopes auto-refresh whenever .gitignore changes.[/]")
|
console.print("[cyan]Scopes auto-refresh whenever .gitignore changes.[/]")
|
||||||
|
|||||||
Reference in New Issue
Block a user