Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 25 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,80 +1,55 @@
# git-explain

**Commit message block?** Run this in your repo after you change files. It suggests `git add` and `git commit` lines you can copy—or apply in one step if you want. **Nothing leaves your machine** unless you turn on AI.
Suggest **conventional** `git add` / `git commit` messages from your changes. **Local heuristics by default** (no network); add **`--ai`** for Google Gemini.

[![PyPI](https://img.shields.io/pypi/v/git-explain.svg?label=pypi)](https://pypi.org/project/git-explain/)
[![GitHub tag](https://img.shields.io/github/v/tag/nazarli-shabnam/git-explain?label=repo)](https://github.com/nazarli-shabnam/git-explain/tags)


---

## Install (Python 3.10+)
## Install & run

```bash
pip install git-explain
cd /path/to/your/git/repo # repo with local changes
git-explain
```

**From source** (this repo):

```bash
pip install -e .
```

**Clone:** install deps (`requirements.txt` is fine), `cd` into the repo, then:

```bash
python -m git_explain
```

Run that from the repo root so Python picks up the `git_explain` folder—no `pip install -e .` needed.
**Using `--ai`?** Put **`GEMINI_API_KEY=…`** (or **`GOOGLE_API_KEY`**) in a **`.env` file at your project’s git root** — the top of the repo you’re working in, not the folder where `git-explain` is installed.

Optional: install a specific tag from GitHub instead of PyPI:

```bash
pip install "git+https://github.com/nazarli-shabnam/git-explain.git@v2.2.1"
```
**Enter** applies the suggested commands; **n** skips (copy only). Pin a release from GitHub:
`pip install "git+https://github.com/nazarli-shabnam/git-explain.git@v2.3.0"` (swap the tag as needed).
Comment thread
nazarli-shabnam marked this conversation as resolved.

---

## Try it
## Flags, keys, and AI

1. In any git repo, change or add a file (not ignored).
2. Run:
```bash
git-explain
```
3. Choose what to include (`all` is fine), read the suggestion, answer **`n`** if you only want to copy commands yourself—nothing bad happens.
| | |
|--|--|
| **Conventional commits** | `feat:`, `fix:`, optional `(scope)`, etc. — see [spec](https://www.conventionalcommits.org/). |
| **`.env`** | `GEMINI_API_KEY` or `GOOGLE_API_KEY` in **`.env` at that repo’s git root** (loaded after the repo is resolved). |
| **Shell (one session)** | Set the variable in the terminal; it overrides `.env` for that window. PowerShell: `$env:GEMINI_API_KEY="…"` then `git-explain --ai`. bash/zsh: `export GEMINI_API_KEY="…"`. |
| **`--auto`** | Apply without the apply prompt. |
| **`--staged-only`** | Commit the index only (no `git add` from the tool). |
| **`--cwd`** | Treat another directory as the git repo root. |

Heuristics guess a sensible type and message from paths and statuses. **No account, no key, no network** for that path.

Suggested commits follow **[Conventional Commits](https://www.conventionalcommits.org/)**—`feat: …`, `fix: …`, optional `(scope)`, and so on—so changelogs and release tools can read them.
**`--ai`** — model sees paths + status. **`--ai --with-diff`** — also sends the diff (more detail, data goes to the API). **`--suggest`** — staged + AI only: prints one `git commit -m "…"` line (no other flags). More: **`git-explain --help`**.

---

## Optional: Gemini

If you want sharper messages, set **`GEMINI_API_KEY`** (or `GOOGLE_API_KEY`) in the environment or a **`.env`** file in the folder where you run the tool.
## If Gemini errors

| Command | In plain terms |
|--------|----------------|
| `git-explain --ai` | AI sees **paths and change type** only (no file contents). |
| `git-explain --ai --with-diff` | AI also sees the **diff**—better detail; only use if you’re OK sending that to the API. |
| `git-explain --suggest` | **Staged only**; prints one plain `git commit -m "…"` line (easy to copy). Needs AI; don’t combine with other flags. |

Everything else (`--auto`, `--staged-only`, `--cwd`, model override, shell completion): **`git-explain --help`**.
- **429 / quota** — [rate limits](https://ai.google.dev/gemini-api/docs/rate-limits).
- **404 / model** — e.g. **`GEMINI_MODEL=gemini-2.5-flash`**; [model list](https://ai.google.dev/api/models).

---

## If Gemini complains

- **429 / quota** — wait a bit, or try the default model; see Google’s [rate limits](https://ai.google.dev/gemini-api/docs/rate-limits).
- **404 / model not found** — set something current, e.g. **`GEMINI_MODEL=gemini-2.5-flash`**, and check their [model list](https://ai.google.dev/api/models).
## Develop

---

## Developers
**Smoke-test a branch:** clone, `pip install -r requirements.txt`, run **`python -m git_explain`** from any git working tree (no `pip install -e .`). **Day-to-day hacking:** `pip install -e ".[dev]"` then `pytest -q`, `ruff check .`, `ruff format --check .`.

```bash
pip install -e ".[dev]"
pytest -q
cd path/to/git-explain
pip install -r requirements.txt
python -m git_explain
```
2 changes: 1 addition & 1 deletion git_explain/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.2.1"
__version__ = "2.3.0"
13 changes: 9 additions & 4 deletions git_explain/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ def run(
except RuntimeError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)

repo_env = repo_root / ".env"
if repo_env.is_file():
load_dotenv(dotenv_path=repo_env, override=False)

Comment on lines +327 to +330

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new repo_root/.env load is likely ineffective because this module already calls load_dotenv() at import time (which can load a different .env based on the current working directory). Since override=False here, any variables loaded earlier (e.g. from a subdir .env) will prevent the repo-root key from being applied, contradicting the README’s “git root” behavior. Consider removing the import-time load_dotenv() and only loading from repo_root (keeping override=False so shell env still wins).

Copilot uses AI. Check for mistakes.
Comment on lines +327 to +330

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading the repo’s .env with load_dotenv() will import all variables from that file into the process environment, which can unexpectedly affect subsequent subprocess calls (e.g., git) beyond just GEMINI_* settings. Since the feature goal is “read API key from .env”, consider reading the file (e.g., via dotenv_values) and only populating GEMINI_API_KEY/GOOGLE_API_KEY (and possibly GEMINI_MODEL) when they aren’t already set.

Copilot uses AI. Check for mistakes.
Comment on lines +327 to +330

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new .env resolution behavior is subtle (repo_root vs CWD precedence, and “shell overrides .env”). There’s existing pytest coverage for cli helpers, but no test validating that GEMINI_API_KEY is picked up from repo_root/.env and that an already-set environment variable is not overridden. Adding a focused unit/integration test would help prevent regressions.

Copilot uses AI. Check for mistakes.
if not combined.strip():
console.print("[yellow]No staged, unstaged, or untracked changes.[/yellow]")
return
Expand Down Expand Up @@ -595,12 +600,12 @@ def _render_plan(pl: list[tuple[str, Suggestion]]) -> str:
do_apply = True
else:
prompt = (
"Apply these commit(s)? (y/n/auto)"
"Apply these commit(s)? (y/n)"
if len(plan) > 1
else "Apply these commands? (y/n/auto)"
else "Apply these commands? (y/n)"
)
choice = typer.prompt(prompt, default="n").strip().lower()
do_apply = choice == "auto" or choice in ("y", "yes")
choice = typer.prompt(prompt, default="y").strip().lower()
Comment thread
nazarli-shabnam marked this conversation as resolved.
do_apply = choice in ("y", "yes")

if do_apply:
for name, sug in plan:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Version Control :: Git",
]
urls = { Homepage = "https://github.com/nazarli-shabnam/git-explain", Source = "https://github.com/nazarli-shabnam/git-explain" }
Expand Down
Loading