Bhargavi commited on
Commit
ad808fd
·
1 Parent(s): 117e1c9

Add Commit vizualizer

Browse files
__pycache__/git_graph.cpython-311.pyc ADDED
Binary file (12.4 kB). View file
 
app.py CHANGED
@@ -1,9 +1,52 @@
 
1
  import gradio as gr
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  if __name__ == "__main__":
9
  demo.launch()
 
1
+ import os
2
  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:
12
+ raise gr.Error(f"'{repo_path}' is not a git repository.")
13
+
14
+
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
+ )
29
+ next_commit_input = gr.Textbox(
30
+ label="Next commit message (preview)",
31
+ placeholder="feat: add new feature",
32
+ scale=2,
33
+ )
34
+
35
+ render_button = gr.Button("Render Graph", variant="primary")
36
+
37
+ graph_output = gr.Plot(label="Branch Graph")
38
+ info_output = gr.Markdown()
39
+
40
+ render_button.click(
41
+ fn=draw_graph,
42
+ inputs=[repo_path_input, next_commit_input],
43
+ outputs=[graph_output, info_output],
44
+ )
45
+ demo.load(
46
+ fn=draw_graph,
47
+ inputs=[repo_path_input, next_commit_input],
48
+ outputs=[graph_output, info_output],
49
+ )
50
 
51
  if __name__ == "__main__":
52
  demo.launch()
git_graph.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_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
+
55
+
56
+ def assign_lanes(commits: list[dict]) -> dict[str, int]:
57
+ lane_by_sha: dict[str, int] = {}
58
+ active_lanes: list[str | None] = []
59
+
60
+ for commit in commits:
61
+ sha = commit["sha"]
62
+ parents = commit["parents"]
63
+
64
+ inherited_lane = None
65
+ for parent_sha in parents:
66
+ if parent_sha in lane_by_sha:
67
+ candidate = lane_by_sha[parent_sha]
68
+ if active_lanes[candidate] == parent_sha:
69
+ inherited_lane = candidate
70
+ break
71
+
72
+ if inherited_lane is not None:
73
+ lane = inherited_lane
74
+ else:
75
+ try:
76
+ lane = active_lanes.index(None)
77
+ except ValueError:
78
+ lane = len(active_lanes)
79
+ active_lanes.append(None)
80
+
81
+ lane_by_sha[sha] = lane
82
+ if lane < len(active_lanes):
83
+ active_lanes[lane] = sha
84
+ else:
85
+ active_lanes.append(sha)
86
+
87
+ return lane_by_sha
88
+
89
+
90
+ def inject_ghost_node(commits: list[dict], head_sha: str, current_branch: str, message: str) -> list[dict]:
91
+ ghost = {
92
+ "sha": "GHOST_NEXT",
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
100
+
101
+
102
+ def build_figure(repo: Repo, commits: list[dict], current_branch: str, next_commit_message: str) -> plt.Figure:
103
+ head_sha = repo.head.commit.hexsha
104
+ ghost_sha = "GHOST_NEXT"
105
+
106
+ if next_commit_message.strip():
107
+ commits = inject_ghost_node(commits, head_sha, current_branch, next_commit_message)
108
+
109
+ lane_by_sha = assign_lanes(commits)
110
+ sha_to_index = {c["sha"]: i for i, c in enumerate(commits)}
111
+
112
+ num_lanes = max(lane_by_sha.values(), default=0) + 1
113
+ fig_width = max(12, 4 + num_lanes * 2)
114
+ fig_height = max(6, len(commits) * 0.6 + 2)
115
+
116
+ fig, ax = plt.subplots(figsize=(fig_width, fig_height))
117
+ fig.patch.set_facecolor(BG_COLOR)
118
+ ax.set_facecolor(BG_COLOR)
119
+
120
+ def node_position(sha: str) -> tuple[float, float]:
121
+ idx = sha_to_index[sha]
122
+ lane = lane_by_sha[sha]
123
+ return lane * X_SPACING, idx * Y_SPACING
124
+
125
+ for commit in commits:
126
+ x_child, y_child = node_position(commit["sha"])
127
+ lane = lane_by_sha[commit["sha"]]
128
+ edge_color = get_branch_color("", lane)
129
+
130
+ for parent_sha in commit["parents"]:
131
+ if parent_sha not in sha_to_index:
132
+ continue
133
+ x_parent, y_parent = node_position(parent_sha)
134
+ is_merge = x_child != x_parent
135
+ ax.annotate(
136
+ "",
137
+ xy=(x_parent, y_parent),
138
+ xytext=(x_child, y_child),
139
+ arrowprops=dict(
140
+ arrowstyle="-|>",
141
+ color=edge_color,
142
+ lw=1.8,
143
+ connectionstyle=f"arc3,rad={'0.3' if is_merge else '0.0'}",
144
+ ),
145
+ zorder=2,
146
+ )
147
+
148
+ for commit in commits:
149
+ x, y = node_position(commit["sha"])
150
+ lane = lane_by_sha[commit["sha"]]
151
+ node_color = get_branch_color("", lane)
152
+ is_ghost = commit["sha"] == ghost_sha
153
+
154
+ if is_ghost:
155
+ ax.plot(x, y, "o", markersize=13, color=GHOST_COLOR, alpha=0.35, zorder=4)
156
+ ax.plot(x, y, "o", markersize=13, color=GHOST_COLOR, fillstyle="none",
157
+ markeredgewidth=2, linestyle="--", zorder=5)
158
+ elif commit["is_head"]:
159
+ ax.plot(x, y, "o", markersize=15, color=node_color, zorder=4)
160
+ ax.plot(x, y, "o", markersize=8, color=HEAD_RING_COLOR, zorder=5)
161
+ else:
162
+ ax.plot(x, y, "o", markersize=10, color=node_color, zorder=4)
163
+
164
+ label_text = f"{commit['short_sha']} {commit['message'][:50]}"
165
+ ax.text(x + 0.35, y, label_text, va="center", ha="left",
166
+ fontsize=8, color=TEXT_COLOR, fontfamily="monospace", zorder=6)
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} ",
176
+ va="center", ha="left",
177
+ fontsize=7, color="white", fontfamily="monospace",
178
+ bbox=dict(boxstyle="round,pad=0.25", facecolor=badge_bg, edgecolor="none"),
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)
185
+ ax.set_title("Git Branch Graph", color=TEXT_COLOR, fontsize=12, pad=12)
186
+ ax.axis("off")
187
+ plt.tight_layout()
188
+
189
+ return fig
190
+
191
+
192
+ 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:
204
+ info_lines.append("**Untracked:** " + ", ".join(f"`{f}`" for f in untracked[:5]))
205
+ if next_commit_message.strip():
206
+ info_lines.append(
207
+ f"**Next commit preview:** `{next_commit_message.strip()}` ← will land on `{current_branch}`"
208
+ )
209
+
210
+ return "\n\n".join(info_lines)
211
+
212
+
213
+ def render_branch_graph(repo_path: str, next_commit_message: str) -> tuple[plt.Figure, str]:
214
+ repo = load_repo(repo_path)
215
+ commits = get_commit_graph(repo)
216
+ commits = tag_commits_with_branches(repo, commits)
217
+ current_branch = repo.active_branch.name if not repo.head.is_detached else "HEAD (detached)"
218
+ fig = build_figure(repo, commits, current_branch, next_commit_message)
219
+ info = build_info(repo, current_branch, next_commit_message)
220
+ return fig, info
requirements.txt CHANGED
@@ -1 +1,3 @@
1
  gradio
 
 
 
1
  gradio
2
+ gitpython
3
+ matplotlib