Bhargavi Claude Sonnet 4.6 commited on
Commit
cf120ca
·
1 Parent(s): ad808fd

Add git branch DAG visualizer with remote branch support

Browse files

- Replaces placeholder greet app with full branch graph visualizer
- Shows local (green) and remote (blue) branch badges on commits
- Ghost node previews where the next commit will land
- Splits logic into git_graph.py, keeps app.py as pure Gradio UI
- Pins dependency versions for reproducible HF Space builds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. README.md +13 -6
  2. app.py +5 -3
  3. git_graph.py +31 -7
  4. requirements.txt +3 -3
README.md CHANGED
@@ -1,15 +1,22 @@
1
  ---
2
- title: Git Commit Ai
3
- emoji: 🦀
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
- short_description: Generate Automatic Git commit message
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
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: Visualize git branch history as a 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
app.py CHANGED
@@ -3,9 +3,11 @@ import gradio as gr
3
  from git import InvalidGitRepositoryError
4
  from git_graph import render_branch_graph
5
 
 
 
6
 
7
  def draw_graph(repo_path: str, next_commit_message: str):
8
- repo_path = repo_path.strip() or os.getcwd()
9
  try:
10
  return render_branch_graph(repo_path, next_commit_message)
11
  except InvalidGitRepositoryError:
@@ -15,14 +17,14 @@ def draw_graph(repo_path: str, next_commit_message: str):
15
  with gr.Blocks(title="Git Branch Visualizer", theme=gr.themes.Base()) as demo:
16
  gr.Markdown("## Git Branch Graph Visualizer")
17
  gr.Markdown(
18
- "Shows all local branches and their commits. "
19
  "Type a commit message to preview **where the next commit lands** (gold ghost node)."
20
  )
21
 
22
  with gr.Row():
23
  repo_path_input = gr.Textbox(
24
  label="Repository path",
25
- value=os.getcwd(),
26
  placeholder="/path/to/your/repo",
27
  scale=3,
28
  )
 
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:
 
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
  )
git_graph.py CHANGED
@@ -41,14 +41,23 @@ def get_commit_graph(repo: Repo) -> list[dict]:
41
 
42
 
43
  def tag_commits_with_branches(repo: Repo, commits: list[dict]) -> list[dict]:
44
- sha_to_labels: dict[str, list[str]] = {}
45
  for branch in repo.branches:
46
  tip = branch.commit.hexsha
47
- sha_to_labels.setdefault(tip, []).append(branch.name)
 
 
 
 
 
 
 
 
48
 
49
  head_sha = repo.head.commit.hexsha
50
  for commit in commits:
51
- commit["labels"] = sha_to_labels.get(commit["sha"], [])
 
52
  commit["is_head"] = commit["sha"] == head_sha
53
  return commits
54
 
@@ -93,7 +102,8 @@ def inject_ghost_node(commits: list[dict], head_sha: str, current_branch: str, m
93
  "short_sha": "next",
94
  "message": message.strip(),
95
  "parents": [head_sha],
96
- "labels": [f"← {current_branch} (next)"],
 
97
  "is_head": False,
98
  }
99
  return [ghost] + commits
@@ -167,9 +177,8 @@ def build_figure(repo: Repo, commits: list[dict], current_branch: str, next_comm
167
 
168
  badge_offset_x = x + 0.35
169
  badge_offset_y = y + 0.22
170
- for label in commit["labels"]:
171
- is_current = label == current_branch
172
- badge_bg = "#9E6A03" if is_current else "#238636"
173
  ax.text(
174
  badge_offset_x, badge_offset_y,
175
  f" {label} ",
@@ -179,6 +188,16 @@ def build_figure(repo: Repo, commits: list[dict], current_branch: str, next_comm
179
  zorder=7,
180
  )
181
  badge_offset_x += len(label) * 0.075 + 0.6
 
 
 
 
 
 
 
 
 
 
182
 
183
  ax.set_xlim(-0.8, num_lanes * X_SPACING + 6)
184
  ax.set_ylim(len(commits) * Y_SPACING - 0.8, 0.8)
@@ -193,11 +212,16 @@ def build_info(repo: Repo, current_branch: str, next_commit_message: str) -> str
193
  head_sha = repo.head.commit.hexsha
194
  staged_files = [item.a_path for item in repo.index.diff("HEAD")]
195
  untracked = repo.untracked_files
 
 
 
196
 
197
  info_lines = [
198
  f"**Current branch:** `{current_branch}`",
199
  f"**HEAD:** `{head_sha[:7]}`",
200
  ]
 
 
201
  if staged_files:
202
  info_lines.append("**Staged (ready to commit):** " + ", ".join(f"`{f}`" for f in staged_files))
203
  if untracked:
 
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
 
 
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
 
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} ",
 
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)
 
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:
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- gradio
2
- gitpython
3
- matplotlib
 
1
+ gradio==6.17.3
2
+ gitpython==3.1.50
3
+ matplotlib==3.10.9