bhargavirengarajan21 commited on
Commit ·
7803188
1
Parent(s): 0bfa946
Added new set of changes
Browse files- README.md +261 -19
- app.py +137 -43
- git-commit-at +1 -0
- git_graph.py +0 -244
README.md
CHANGED
|
@@ -1,22 +1,264 @@
|
|
| 1 |
-
--
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
-
- Remote tracking branches shown with **blue** badges
|
| 21 |
-
- **HEAD** commit highlighted with a white ring
|
| 22 |
-
- Type a commit message to preview the **gold ghost node** — shows exactly where the next commit will land
|
|
|
|
| 1 |
+
# git-commit-app 🎯
|
| 2 |
+
|
| 3 |
+
**A suite of AI-powered git commit tools** for effortless, consistent commit messages.
|
| 4 |
+
|
| 5 |
+
This monorepo contains multiple projects for generating and managing git commits:
|
| 6 |
+
|
| 7 |
+
## Community
|
| 8 |
+
|
| 9 |
+
- [Reddit: I made a CLI that generates conventional commit messages locally using AI](https://www.reddit.com/user/bhargavi_r/comments/1u6v9uc/i_made_a_cli_that_generates_conventional_commit/)
|
| 10 |
+
|
| 11 |
+
## Projects
|
| 12 |
+
|
| 13 |
+
### 🚀 [git-commit-at](./git-commit-at)
|
| 14 |
+
|
| 15 |
+
**AI-powered commit message generator with interactive CLI and web UI**
|
| 16 |
+
|
| 17 |
+
- Generate conventional commit messages using AI (Ollama)
|
| 18 |
+
- Interactive selection from 3 AI-suggested options
|
| 19 |
+
- Web UI for login, commit history, and branch visualization
|
| 20 |
+
- Session persistence and Redis caching
|
| 21 |
+
- Docker Compose setup with Ollama, Redis, and Gradio
|
| 22 |
+
|
| 23 |
+
**Quick start:**
|
| 24 |
+
```bash
|
| 25 |
+
cd git-commit-at
|
| 26 |
+
npm install
|
| 27 |
+
npm start
|
| 28 |
+
git-commit-at
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
**Features:**
|
| 32 |
+
- ✅ Conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
|
| 33 |
+
- ✅ Custom commit format templates
|
| 34 |
+
- ✅ Ticket number integration
|
| 35 |
+
- ✅ Live branch graph visualization
|
| 36 |
+
- ✅ Commit history tracking
|
| 37 |
+
|
| 38 |
+
## Directory Structure
|
| 39 |
+
|
| 40 |
+
```
|
| 41 |
+
git-commit-app/
|
| 42 |
+
├── git-commit-at/ # npm package — CLI + bundled web UI
|
| 43 |
+
│ ├── index.js # Core commit logic (Ollama AI)
|
| 44 |
+
│ ├── wrapper.js # CLI orchestrator
|
| 45 |
+
│ ├── docker-compose.yml # All services (Ollama, Redis, Gradio)
|
| 46 |
+
│ ├── Dockerfile # Node app container
|
| 47 |
+
│ ├── entrypoint.sh # Docker setup script
|
| 48 |
+
│ ├── gradio/ # Bundled web UI (Python/Gradio)
|
| 49 |
+
│ │ ├── app.py # Login + history + branch visualizer
|
| 50 |
+
│ │ ├── auth_service.py # Redis-based user auth
|
| 51 |
+
│ │ ├── generate_graph.py
|
| 52 |
+
│ │ ├── git_graph.py
|
| 53 |
+
│ │ └── Dockerfile
|
| 54 |
+
│ └── package.json
|
| 55 |
+
│
|
| 56 |
+
└── README.md # This file
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Getting Started
|
| 60 |
+
|
| 61 |
+
### Prerequisites
|
| 62 |
+
|
| 63 |
+
- Docker & Docker Compose
|
| 64 |
+
- Node.js 18+ (for development)
|
| 65 |
+
- Git
|
| 66 |
+
|
| 67 |
+
### Installation
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
# Clone the repo
|
| 71 |
+
git clone https://huggingface.co/spaces/build-small-hackathon/git-commit-app
|
| 72 |
+
cd git-commit-app
|
| 73 |
+
|
| 74 |
+
# Navigate to the project you want to use
|
| 75 |
+
cd git-commit-at
|
| 76 |
+
|
| 77 |
+
# Install and run
|
| 78 |
+
npm install
|
| 79 |
+
npm start
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### First Use
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
# Stage your changes
|
| 86 |
+
git add .
|
| 87 |
+
|
| 88 |
+
# Generate commit message
|
| 89 |
+
git-commit-at
|
| 90 |
+
|
| 91 |
+
# Follow the interactive prompts
|
| 92 |
+
# - Log in or register
|
| 93 |
+
# - Select a suggested message
|
| 94 |
+
# - Confirm and commit
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## Architecture
|
| 98 |
+
|
| 99 |
+
```
|
| 100 |
+
┌──────────────────────────────────────────────┐
|
| 101 |
+
│ git-commit-at (npm package) │
|
| 102 |
+
├──────────────────────────────────────────────┤
|
| 103 |
+
│ │
|
| 104 |
+
│ CLI (Node.js) ←→ Docker Compose │
|
| 105 |
+
│ ├── Ollama (AI) │
|
| 106 |
+
│ ├── Redis (cache) │
|
| 107 |
+
│ ├── Gradio (web UI) │
|
| 108 |
+
│ └── commit-at (Node) │
|
| 109 |
+
│ │
|
| 110 |
+
└──────────────────────────────────────────────┘
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
## Features Overview
|
| 114 |
+
|
| 115 |
+
### git-commit-at
|
| 116 |
+
|
| 117 |
+
- 🤖 **AI Suggestions** — 3 smart commit message options
|
| 118 |
+
- 🎯 **Conventional Format** — Follows commit standards
|
| 119 |
+
- 🌐 **Web UI** — Login, history, branch visualization
|
| 120 |
+
- 📝 **Custom Formats** — Define your own commit template
|
| 121 |
+
- 🔐 **Session Persistence** — Stay logged in
|
| 122 |
+
- 🐳 **Docker Ready** — One-click setup
|
| 123 |
+
- 📊 **Branch Visualizer** — See your git graph live
|
| 124 |
+
|
| 125 |
+
## Development
|
| 126 |
+
|
| 127 |
+
### Project Docs
|
| 128 |
+
|
| 129 |
+
- **git-commit-at**: [README](./git-commit-at/README.md) — Full setup, troubleshooting, contributing
|
| 130 |
+
|
| 131 |
+
### Development Workflow
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
# Install all dependencies
|
| 135 |
+
npm install
|
| 136 |
+
|
| 137 |
+
# Run services
|
| 138 |
+
docker compose up -d
|
| 139 |
+
|
| 140 |
+
# Access web UI
|
| 141 |
+
# http://localhost:7860
|
| 142 |
|
| 143 |
+
# Run CLI
|
| 144 |
+
node wrapper.js
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### File Structure (git-commit-at)
|
| 148 |
+
|
| 149 |
+
| File | Purpose |
|
| 150 |
+
|------|---------|
|
| 151 |
+
| `wrapper.js` | CLI entry; manages Docker services |
|
| 152 |
+
| `index.js` | Main commit logic |
|
| 153 |
+
| `diff-parser.js` | Parses git diffs |
|
| 154 |
+
| `entrypoint.sh` | Docker setup |
|
| 155 |
+
| `gradio/app.py` | Web UI |
|
| 156 |
+
| `gradio/git_graph.py` | Branch visualization |
|
| 157 |
+
|
| 158 |
+
## Contributing
|
| 159 |
+
|
| 160 |
+
We welcome contributions! Here's how:
|
| 161 |
+
|
| 162 |
+
1. **Fork** the repo
|
| 163 |
+
2. **Create a branch** for your feature: `git checkout -b feature/my-feature`
|
| 164 |
+
3. **Make changes** and test locally
|
| 165 |
+
4. **Use git-commit-at** for your commits 😉
|
| 166 |
+
5. **Push** and open a PR
|
| 167 |
+
|
| 168 |
+
### Commit Guidelines
|
| 169 |
+
|
| 170 |
+
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
| 171 |
+
- `feat:` — New feature
|
| 172 |
+
- `fix:` — Bug fix
|
| 173 |
+
- `docs:` — Documentation
|
| 174 |
+
- `refactor:` — Code refactor
|
| 175 |
+
- `test:` — Tests
|
| 176 |
+
- `chore:` — Build, dependencies, etc.
|
| 177 |
+
|
| 178 |
+
## Troubleshooting
|
| 179 |
+
|
| 180 |
+
### General Issues
|
| 181 |
+
|
| 182 |
+
| Problem | Solution |
|
| 183 |
+
|---------|----------|
|
| 184 |
+
| Docker won't start | Check if Docker daemon is running |
|
| 185 |
+
| Port already in use | Change port in `docker-compose.yml` |
|
| 186 |
+
| "Not a git repo" | Run from inside a git repository |
|
| 187 |
+
|
| 188 |
+
### git-commit-at Issues
|
| 189 |
+
|
| 190 |
+
See [git-commit-at Troubleshooting](./git-commit-at/README.md#troubleshooting)
|
| 191 |
+
|
| 192 |
+
## Performance
|
| 193 |
+
|
| 194 |
+
| Operation | Time |
|
| 195 |
+
|-----------|------|
|
| 196 |
+
| First run (download models) | ~30s |
|
| 197 |
+
| Generate suggestions | ~5-10s |
|
| 198 |
+
| Web UI load | <1s |
|
| 199 |
+
|
| 200 |
+
## Environment Setup
|
| 201 |
+
|
| 202 |
+
### Using Docker (Recommended)
|
| 203 |
+
|
| 204 |
+
Everything runs in containers—no local setup needed!
|
| 205 |
+
|
| 206 |
+
```bash
|
| 207 |
+
docker compose up -d
|
| 208 |
+
npm start
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### Local Development
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
# Set up Node environment
|
| 215 |
+
node --version # Should be 18+
|
| 216 |
+
|
| 217 |
+
# Install deps
|
| 218 |
+
npm install
|
| 219 |
+
|
| 220 |
+
# Start Docker services only
|
| 221 |
+
docker compose up -d ollama redis gradio
|
| 222 |
+
|
| 223 |
+
# Run app
|
| 224 |
+
node wrapper.js
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
## FAQ
|
| 228 |
+
|
| 229 |
+
**Q: Do I need Ollama installed locally?**
|
| 230 |
+
A: No! Ollama runs in a Docker container. Just ensure Docker is running.
|
| 231 |
+
|
| 232 |
+
**Q: Can I use a different AI model?**
|
| 233 |
+
A: Yes! Edit the `MODEL` variable in `git-commit-at/index.js`.
|
| 234 |
+
|
| 235 |
+
**Q: Does this work offline?**
|
| 236 |
+
A: After the first run (which downloads the AI model), yes!
|
| 237 |
+
|
| 238 |
+
**Q: Can I use this without the web UI?**
|
| 239 |
+
A: Yes! Just run `git-commit-at` from the CLI.
|
| 240 |
+
|
| 241 |
+
**Q: How do I customize the commit format?**
|
| 242 |
+
A: You'll be prompted each time. Or edit the format in the Gradio UI after logging in.
|
| 243 |
+
|
| 244 |
+
## License
|
| 245 |
+
|
| 246 |
+
MIT — See [LICENSE](LICENSE) for details
|
| 247 |
+
|
| 248 |
+
## Links
|
| 249 |
+
|
| 250 |
+
- 🐳 Docker Hub: [git-commit-at](https://hub.docker.com)
|
| 251 |
+
- 🤗 Hugging Face: [Spaces](https://huggingface.co/spaces/build-small-hackathon/git-commit-a)
|
| 252 |
+
- 📦 NPM: [git-commit-at](https://npmjs.com/package/git-commit-at)
|
| 253 |
+
|
| 254 |
+
## Support
|
| 255 |
+
|
| 256 |
+
- 📖 **Docs**: Check individual project READMEs
|
| 257 |
+
- 🐛 **Issues**: Report on GitHub
|
| 258 |
+
- 💬 **Discussions**: Ask questions in GitHub Discussions
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
|
| 262 |
+
**Made with ❤️ for developers who want better commits, faster.**
|
| 263 |
|
| 264 |
+
🚀 Start generating smarter commits today!
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,54 +1,148 @@
|
|
| 1 |
-
import os
|
| 2 |
import gradio as gr
|
| 3 |
-
from git import InvalidGitRepositoryError
|
| 4 |
-
from git_graph import render_branch_graph
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
repo_path = repo_path.strip() or DEFAULT_REPO_PATH
|
| 11 |
-
try:
|
| 12 |
-
return render_branch_graph(repo_path, next_commit_message)
|
| 13 |
-
except InvalidGitRepositoryError:
|
| 14 |
-
raise gr.Error(f"'{repo_path}' is not a git repository.")
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
with gr.Row():
|
| 25 |
-
|
| 26 |
-
label="Repository path",
|
| 27 |
-
value=DEFAULT_REPO_PATH,
|
| 28 |
-
placeholder="/path/to/your/repo",
|
| 29 |
-
scale=3,
|
| 30 |
-
)
|
| 31 |
-
next_commit_input = gr.Textbox(
|
| 32 |
-
label="Next commit message (preview)",
|
| 33 |
-
placeholder="feat: add new feature",
|
| 34 |
-
scale=2,
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
render_button = gr.Button("Render Graph", variant="primary")
|
| 38 |
-
|
| 39 |
-
graph_output = gr.Plot(label="Branch Graph")
|
| 40 |
-
info_output = gr.Markdown()
|
| 41 |
-
|
| 42 |
-
render_button.click(
|
| 43 |
-
fn=draw_graph,
|
| 44 |
-
inputs=[repo_path_input, next_commit_input],
|
| 45 |
-
outputs=[graph_output, info_output],
|
| 46 |
-
)
|
| 47 |
-
demo.load(
|
| 48 |
-
fn=draw_graph,
|
| 49 |
-
inputs=[repo_path_input, next_commit_input],
|
| 50 |
-
outputs=[graph_output, info_output],
|
| 51 |
-
)
|
| 52 |
|
| 53 |
if __name__ == "__main__":
|
| 54 |
demo.launch()
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
INSTALL_CMD = "npm install -g git-commit-at"
|
| 4 |
+
NPM_URL = "https://www.npmjs.com/package/git-commit-at"
|
| 5 |
+
GITHUB_URL = "https://github.com/bhargavirengarajan21/git-commit-at"
|
| 6 |
+
DEMO_URL = "#" # TBH
|
| 7 |
|
| 8 |
+
ABOUT_MD = """
|
| 9 |
+
# git-commit-at
|
| 10 |
|
| 11 |
+
**AI-powered git commit message generator** — local, private, no API key needed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
`git-commit-at` analyzes your staged changes and suggests 3 conventional commit messages
|
| 14 |
+
using a locally-running AI model (Ollama). Pick one, customize it, and commit — all from
|
| 15 |
+
the terminal.
|
| 16 |
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## Why git-commit-at?
|
| 20 |
+
|
| 21 |
+
| | git-commit-at | Manual commits | Cloud AI tools |
|
| 22 |
+
|---|---|---|---|
|
| 23 |
+
| **Private** | ✅ 100% local | ✅ | ❌ sends code to cloud |
|
| 24 |
+
| **No API key** | ✅ | ✅ | ❌ requires subscription |
|
| 25 |
+
| **Conventional commits** | ✅ auto | ❌ manual discipline | ⚠️ sometimes |
|
| 26 |
+
| **Branch visualizer** | ✅ built-in | ❌ | ❌ |
|
| 27 |
+
| **Commit history** | ✅ tracked | ❌ | ❌ |
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Install
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
npm install -g git-commit-at
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Requires: **Docker** (all AI/backend services run in containers — no extra setup).
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## How to use
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
# 1. Stage your changes
|
| 45 |
+
git add .
|
| 46 |
+
|
| 47 |
+
# 2. Run the tool
|
| 48 |
+
git-commit-at
|
| 49 |
+
|
| 50 |
+
# 3. First time: register/login at http://localhost:7860
|
| 51 |
+
# 4. Pick a suggested commit message
|
| 52 |
+
# 5. Confirm — done!
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
On first run, Docker starts Ollama, Redis, and the Gradio UI automatically.
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Features
|
| 60 |
+
|
| 61 |
+
- 🤖 **3 AI suggestions** per run, tailored to your actual diff
|
| 62 |
+
- 📋 **Conventional commits** — `feat:`, `fix:`, `refactor:`, `chore:`, etc.
|
| 63 |
+
- 🎫 **Ticket integration** — prefix messages with JIRA/Linear ticket numbers
|
| 64 |
+
- 🌿 **Branch visualizer** — live git branch DAG in the web UI
|
| 65 |
+
- 📊 **Commit history** — track everything you've committed across repos
|
| 66 |
+
- 🔐 **Session persistence** — stay logged in between runs
|
| 67 |
+
- 🐳 **Zero local dependencies** — everything runs in Docker
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## Numbers
|
| 72 |
+
|
| 73 |
+
*(TBH — to be updated with real metrics)*
|
| 74 |
+
|
| 75 |
+
| Metric | Value |
|
| 76 |
+
|--------|-------|
|
| 77 |
+
| Average suggestion time | ~5–10 s |
|
| 78 |
+
| First-run setup time | ~30 s |
|
| 79 |
+
| AI model size | ~1 GB (qwen2.5-coder:1.5b) |
|
| 80 |
+
| Runs entirely offline after setup | ✅ |
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## Links
|
| 85 |
+
|
| 86 |
+
- 📦 **npm**: [{npm}]({npm})
|
| 87 |
+
- 💻 **GitHub**: [{gh}]({gh})
|
| 88 |
+
- 🎬 **Demo**: [Coming soon]({demo})
|
| 89 |
+
""".format(npm=NPM_URL, gh=GITHUB_URL, demo=DEMO_URL)
|
| 90 |
+
|
| 91 |
+
QUICKSTART_MD = """
|
| 92 |
+
## Quick Start
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
# Install
|
| 96 |
+
npm install -g git-commit-at
|
| 97 |
+
|
| 98 |
+
# Go to any git repo
|
| 99 |
+
cd your-project
|
| 100 |
+
git add .
|
| 101 |
+
|
| 102 |
+
# Generate commit message
|
| 103 |
+
git-commit-at
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
The first run starts all Docker services and opens the login UI at `http://localhost:7860`.
|
| 107 |
+
After login, you'll see 3 commit message suggestions based on your staged diff.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
HOW_IT_WORKS_MD = """
|
| 111 |
+
## How it works
|
| 112 |
+
|
| 113 |
+
```
|
| 114 |
+
git-commit-at (CLI)
|
| 115 |
+
│
|
| 116 |
+
├── Checks Docker services
|
| 117 |
+
│ ├── Ollama (AI model, port 11434)
|
| 118 |
+
│ ├── Redis (session + cache, port 6379)
|
| 119 |
+
│ └── Gradio (web UI, port 7860)
|
| 120 |
+
│
|
| 121 |
+
├── Waits for login at http://localhost:7860
|
| 122 |
+
│
|
| 123 |
+
└── Runs commit flow
|
| 124 |
+
├── Reads git diff of staged files
|
| 125 |
+
├── Sends diff to Ollama (qwen2.5-coder:1.5b)
|
| 126 |
+
├── Streams 3 commit suggestions
|
| 127 |
+
├── Prompts: ticket number? format?
|
| 128 |
+
├── You pick a message
|
| 129 |
+
└── git commit -m "..."
|
| 130 |
+
```
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
with gr.Blocks(title="git-commit-at", theme=gr.themes.Monochrome()) as demo:
|
| 134 |
+
with gr.Row():
|
| 135 |
+
with gr.Column(scale=2):
|
| 136 |
+
gr.Markdown(ABOUT_MD)
|
| 137 |
+
with gr.Column(scale=1):
|
| 138 |
+
gr.Markdown("## Install now")
|
| 139 |
+
gr.Code(value=INSTALL_CMD, language="bash", label="npm")
|
| 140 |
+
gr.Markdown(f"[View on npm]({NPM_URL}) · [GitHub]({GITHUB_URL})")
|
| 141 |
+
gr.Markdown("---")
|
| 142 |
+
gr.Markdown(QUICKSTART_MD)
|
| 143 |
|
| 144 |
with gr.Row():
|
| 145 |
+
gr.Markdown(HOW_IT_WORKS_MD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
if __name__ == "__main__":
|
| 148 |
demo.launch()
|
git-commit-at
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Subproject commit fb7169fbc27f0b23149ad3818b290e698dfa9203
|
git_graph.py
DELETED
|
@@ -1,244 +0,0 @@
|
|
| 1 |
-
import matplotlib
|
| 2 |
-
matplotlib.use("Agg")
|
| 3 |
-
import matplotlib.pyplot as plt
|
| 4 |
-
from git import Repo, InvalidGitRepositoryError
|
| 5 |
-
|
| 6 |
-
BRANCH_COLORS = {
|
| 7 |
-
"main": "#4C8BF5",
|
| 8 |
-
"master": "#4C8BF5",
|
| 9 |
-
}
|
| 10 |
-
FALLBACK_COLORS = ["#2ECC71", "#E84B3A", "#F39C12", "#9B59B6", "#1ABC9C", "#E67E22"]
|
| 11 |
-
GHOST_COLOR = "#FFD700"
|
| 12 |
-
HEAD_RING_COLOR = "#FFFFFF"
|
| 13 |
-
BG_COLOR = "#0D1117"
|
| 14 |
-
TEXT_COLOR = "#C9D1D9"
|
| 15 |
-
X_SPACING = 1.5
|
| 16 |
-
Y_SPACING = -0.7
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def get_branch_color(branch_name: str, index: int) -> str:
|
| 20 |
-
return BRANCH_COLORS.get(branch_name, FALLBACK_COLORS[index % len(FALLBACK_COLORS)])
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def load_repo(repo_path: str) -> Repo:
|
| 24 |
-
return Repo(repo_path)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def get_commit_graph(repo: Repo) -> list[dict]:
|
| 28 |
-
seen = set()
|
| 29 |
-
commits = []
|
| 30 |
-
for commit in repo.iter_commits("--all"):
|
| 31 |
-
if commit.hexsha in seen:
|
| 32 |
-
continue
|
| 33 |
-
seen.add(commit.hexsha)
|
| 34 |
-
commits.append({
|
| 35 |
-
"sha": commit.hexsha,
|
| 36 |
-
"short_sha": commit.hexsha[:7],
|
| 37 |
-
"message": commit.message.strip().splitlines()[0],
|
| 38 |
-
"parents": [p.hexsha for p in commit.parents],
|
| 39 |
-
})
|
| 40 |
-
return commits
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def tag_commits_with_branches(repo: Repo, commits: list[dict]) -> list[dict]:
|
| 44 |
-
sha_to_local: dict[str, list[str]] = {}
|
| 45 |
-
for branch in repo.branches:
|
| 46 |
-
tip = branch.commit.hexsha
|
| 47 |
-
sha_to_local.setdefault(tip, []).append(branch.name)
|
| 48 |
-
|
| 49 |
-
sha_to_remote: dict[str, list[str]] = {}
|
| 50 |
-
for remote in repo.remotes:
|
| 51 |
-
for ref in remote.refs:
|
| 52 |
-
if ref.name.endswith("/HEAD"):
|
| 53 |
-
continue
|
| 54 |
-
tip = ref.commit.hexsha
|
| 55 |
-
sha_to_remote.setdefault(tip, []).append(ref.name)
|
| 56 |
-
|
| 57 |
-
head_sha = repo.head.commit.hexsha
|
| 58 |
-
for commit in commits:
|
| 59 |
-
commit["local_labels"] = sha_to_local.get(commit["sha"], [])
|
| 60 |
-
commit["remote_labels"] = sha_to_remote.get(commit["sha"], [])
|
| 61 |
-
commit["is_head"] = commit["sha"] == head_sha
|
| 62 |
-
return commits
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def assign_lanes(commits: list[dict]) -> dict[str, int]:
|
| 66 |
-
lane_by_sha: dict[str, int] = {}
|
| 67 |
-
active_lanes: list[str | None] = []
|
| 68 |
-
|
| 69 |
-
for commit in commits:
|
| 70 |
-
sha = commit["sha"]
|
| 71 |
-
parents = commit["parents"]
|
| 72 |
-
|
| 73 |
-
inherited_lane = None
|
| 74 |
-
for parent_sha in parents:
|
| 75 |
-
if parent_sha in lane_by_sha:
|
| 76 |
-
candidate = lane_by_sha[parent_sha]
|
| 77 |
-
if active_lanes[candidate] == parent_sha:
|
| 78 |
-
inherited_lane = candidate
|
| 79 |
-
break
|
| 80 |
-
|
| 81 |
-
if inherited_lane is not None:
|
| 82 |
-
lane = inherited_lane
|
| 83 |
-
else:
|
| 84 |
-
try:
|
| 85 |
-
lane = active_lanes.index(None)
|
| 86 |
-
except ValueError:
|
| 87 |
-
lane = len(active_lanes)
|
| 88 |
-
active_lanes.append(None)
|
| 89 |
-
|
| 90 |
-
lane_by_sha[sha] = lane
|
| 91 |
-
if lane < len(active_lanes):
|
| 92 |
-
active_lanes[lane] = sha
|
| 93 |
-
else:
|
| 94 |
-
active_lanes.append(sha)
|
| 95 |
-
|
| 96 |
-
return lane_by_sha
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def inject_ghost_node(commits: list[dict], head_sha: str, current_branch: str, message: str) -> list[dict]:
|
| 100 |
-
ghost = {
|
| 101 |
-
"sha": "GHOST_NEXT",
|
| 102 |
-
"short_sha": "next",
|
| 103 |
-
"message": message.strip(),
|
| 104 |
-
"parents": [head_sha],
|
| 105 |
-
"local_labels": [f"← {current_branch} (next)"],
|
| 106 |
-
"remote_labels": [],
|
| 107 |
-
"is_head": False,
|
| 108 |
-
}
|
| 109 |
-
return [ghost] + commits
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def build_figure(repo: Repo, commits: list[dict], current_branch: str, next_commit_message: str) -> plt.Figure:
|
| 113 |
-
head_sha = repo.head.commit.hexsha
|
| 114 |
-
ghost_sha = "GHOST_NEXT"
|
| 115 |
-
|
| 116 |
-
if next_commit_message.strip():
|
| 117 |
-
commits = inject_ghost_node(commits, head_sha, current_branch, next_commit_message)
|
| 118 |
-
|
| 119 |
-
lane_by_sha = assign_lanes(commits)
|
| 120 |
-
sha_to_index = {c["sha"]: i for i, c in enumerate(commits)}
|
| 121 |
-
|
| 122 |
-
num_lanes = max(lane_by_sha.values(), default=0) + 1
|
| 123 |
-
fig_width = max(12, 4 + num_lanes * 2)
|
| 124 |
-
fig_height = max(6, len(commits) * 0.6 + 2)
|
| 125 |
-
|
| 126 |
-
fig, ax = plt.subplots(figsize=(fig_width, fig_height))
|
| 127 |
-
fig.patch.set_facecolor(BG_COLOR)
|
| 128 |
-
ax.set_facecolor(BG_COLOR)
|
| 129 |
-
|
| 130 |
-
def node_position(sha: str) -> tuple[float, float]:
|
| 131 |
-
idx = sha_to_index[sha]
|
| 132 |
-
lane = lane_by_sha[sha]
|
| 133 |
-
return lane * X_SPACING, idx * Y_SPACING
|
| 134 |
-
|
| 135 |
-
for commit in commits:
|
| 136 |
-
x_child, y_child = node_position(commit["sha"])
|
| 137 |
-
lane = lane_by_sha[commit["sha"]]
|
| 138 |
-
edge_color = get_branch_color("", lane)
|
| 139 |
-
|
| 140 |
-
for parent_sha in commit["parents"]:
|
| 141 |
-
if parent_sha not in sha_to_index:
|
| 142 |
-
continue
|
| 143 |
-
x_parent, y_parent = node_position(parent_sha)
|
| 144 |
-
is_merge = x_child != x_parent
|
| 145 |
-
ax.annotate(
|
| 146 |
-
"",
|
| 147 |
-
xy=(x_parent, y_parent),
|
| 148 |
-
xytext=(x_child, y_child),
|
| 149 |
-
arrowprops=dict(
|
| 150 |
-
arrowstyle="-|>",
|
| 151 |
-
color=edge_color,
|
| 152 |
-
lw=1.8,
|
| 153 |
-
connectionstyle=f"arc3,rad={'0.3' if is_merge else '0.0'}",
|
| 154 |
-
),
|
| 155 |
-
zorder=2,
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
for commit in commits:
|
| 159 |
-
x, y = node_position(commit["sha"])
|
| 160 |
-
lane = lane_by_sha[commit["sha"]]
|
| 161 |
-
node_color = get_branch_color("", lane)
|
| 162 |
-
is_ghost = commit["sha"] == ghost_sha
|
| 163 |
-
|
| 164 |
-
if is_ghost:
|
| 165 |
-
ax.plot(x, y, "o", markersize=13, color=GHOST_COLOR, alpha=0.35, zorder=4)
|
| 166 |
-
ax.plot(x, y, "o", markersize=13, color=GHOST_COLOR, fillstyle="none",
|
| 167 |
-
markeredgewidth=2, linestyle="--", zorder=5)
|
| 168 |
-
elif commit["is_head"]:
|
| 169 |
-
ax.plot(x, y, "o", markersize=15, color=node_color, zorder=4)
|
| 170 |
-
ax.plot(x, y, "o", markersize=8, color=HEAD_RING_COLOR, zorder=5)
|
| 171 |
-
else:
|
| 172 |
-
ax.plot(x, y, "o", markersize=10, color=node_color, zorder=4)
|
| 173 |
-
|
| 174 |
-
label_text = f"{commit['short_sha']} {commit['message'][:50]}"
|
| 175 |
-
ax.text(x + 0.35, y, label_text, va="center", ha="left",
|
| 176 |
-
fontsize=8, color=TEXT_COLOR, fontfamily="monospace", zorder=6)
|
| 177 |
-
|
| 178 |
-
badge_offset_x = x + 0.35
|
| 179 |
-
badge_offset_y = y + 0.22
|
| 180 |
-
for label in commit["local_labels"]:
|
| 181 |
-
badge_bg = "#9E6A03" if label == current_branch else "#238636"
|
| 182 |
-
ax.text(
|
| 183 |
-
badge_offset_x, badge_offset_y,
|
| 184 |
-
f" {label} ",
|
| 185 |
-
va="center", ha="left",
|
| 186 |
-
fontsize=7, color="white", fontfamily="monospace",
|
| 187 |
-
bbox=dict(boxstyle="round,pad=0.25", facecolor=badge_bg, edgecolor="none"),
|
| 188 |
-
zorder=7,
|
| 189 |
-
)
|
| 190 |
-
badge_offset_x += len(label) * 0.075 + 0.6
|
| 191 |
-
for label in commit["remote_labels"]:
|
| 192 |
-
ax.text(
|
| 193 |
-
badge_offset_x, badge_offset_y,
|
| 194 |
-
f" {label} ",
|
| 195 |
-
va="center", ha="left",
|
| 196 |
-
fontsize=7, color="white", fontfamily="monospace",
|
| 197 |
-
bbox=dict(boxstyle="round,pad=0.25", facecolor="#1F6FEB", edgecolor="none"),
|
| 198 |
-
zorder=7,
|
| 199 |
-
)
|
| 200 |
-
badge_offset_x += len(label) * 0.075 + 0.6
|
| 201 |
-
|
| 202 |
-
ax.set_xlim(-0.8, num_lanes * X_SPACING + 6)
|
| 203 |
-
ax.set_ylim(len(commits) * Y_SPACING - 0.8, 0.8)
|
| 204 |
-
ax.set_title("Git Branch Graph", color=TEXT_COLOR, fontsize=12, pad=12)
|
| 205 |
-
ax.axis("off")
|
| 206 |
-
plt.tight_layout()
|
| 207 |
-
|
| 208 |
-
return fig
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def build_info(repo: Repo, current_branch: str, next_commit_message: str) -> str:
|
| 212 |
-
head_sha = repo.head.commit.hexsha
|
| 213 |
-
staged_files = [item.a_path for item in repo.index.diff("HEAD")]
|
| 214 |
-
untracked = repo.untracked_files
|
| 215 |
-
remote_branches = [
|
| 216 |
-
ref.name for remote in repo.remotes for ref in remote.refs if not ref.name.endswith("/HEAD")
|
| 217 |
-
]
|
| 218 |
-
|
| 219 |
-
info_lines = [
|
| 220 |
-
f"**Current branch:** `{current_branch}`",
|
| 221 |
-
f"**HEAD:** `{head_sha[:7]}`",
|
| 222 |
-
]
|
| 223 |
-
if remote_branches:
|
| 224 |
-
info_lines.append("**Remote branches:** " + ", ".join(f"`{r}`" for r in remote_branches))
|
| 225 |
-
if staged_files:
|
| 226 |
-
info_lines.append("**Staged (ready to commit):** " + ", ".join(f"`{f}`" for f in staged_files))
|
| 227 |
-
if untracked:
|
| 228 |
-
info_lines.append("**Untracked:** " + ", ".join(f"`{f}`" for f in untracked[:5]))
|
| 229 |
-
if next_commit_message.strip():
|
| 230 |
-
info_lines.append(
|
| 231 |
-
f"**Next commit preview:** `{next_commit_message.strip()}` ← will land on `{current_branch}`"
|
| 232 |
-
)
|
| 233 |
-
|
| 234 |
-
return "\n\n".join(info_lines)
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
def render_branch_graph(repo_path: str, next_commit_message: str) -> tuple[plt.Figure, str]:
|
| 238 |
-
repo = load_repo(repo_path)
|
| 239 |
-
commits = get_commit_graph(repo)
|
| 240 |
-
commits = tag_commits_with_branches(repo, commits)
|
| 241 |
-
current_branch = repo.active_branch.name if not repo.head.is_detached else "HEAD (detached)"
|
| 242 |
-
fig = build_figure(repo, commits, current_branch, next_commit_message)
|
| 243 |
-
info = build_info(repo, current_branch, next_commit_message)
|
| 244 |
-
return fig, info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|