bhargavirengarajan21 commited on
Commit
7803188
·
1 Parent(s): 0bfa946

Added new set of changes

Browse files
Files changed (4) hide show
  1. README.md +261 -19
  2. app.py +137 -43
  3. git-commit-at +1 -0
  4. git_graph.py +0 -244
README.md CHANGED
@@ -1,22 +1,264 @@
1
- ---
2
- title: Git Branch Visualizer
3
- emoji: 🌿
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.11'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: Git branch DAG with next-commit preview
13
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- # Git Branch Graph Visualizer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- Visualize your git repository's branch history as an interactive DAG.
18
 
19
- - Local branches shown with **green** badges
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
- DEFAULT_REPO_PATH = os.environ.get("REPO_PATH", os.getcwd())
 
 
 
7
 
 
 
8
 
9
- def draw_graph(repo_path: str, next_commit_message: str):
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
- with gr.Blocks(title="Git Branch Visualizer", theme=gr.themes.Base()) as demo:
18
- gr.Markdown("## Git Branch Graph Visualizer")
19
- gr.Markdown(
20
- "Shows all local **and remote** branches as a commit DAG. "
21
- "Type a commit message to preview **where the next commit lands** (gold ghost node)."
22
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  with gr.Row():
25
- repo_path_input = gr.Textbox(
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