-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit_graph.py
More file actions
334 lines (279 loc) · 14.1 KB
/
Copy pathgit_graph.py
File metadata and controls
334 lines (279 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import git
from datetime import datetime, timedelta
from collections import defaultdict
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import pandas as pd
import numpy as np
from matplotlib.colors import ListedColormap, BoundaryNorm
import os
import sys
class GitGraphApp:
def __init__(self, root):
self.root = root
self.root.title("Git Contribution Graph")
self.repo_path = None
self.activity = {}
self.start_date = datetime.now().date() - timedelta(days=365)
self.end_date = datetime.now().date()
self.email = None
self.stats_label = None # NEW
if len(sys.argv) > 1:
provided_path = sys.argv[1]
try:
git.Repo(provided_path)
self.repo_path = provided_path
self.root.after(100, self.update_graph_with_focus)
except git.InvalidGitRepositoryError:
messagebox.showerror("Error", f"Invalid Git repository at {provided_path}. Please select a valid repository.")
self.load_repo()
else:
self.load_repo()
self.setup_ui()
def setup_ui(self):
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True)
self.control_frame = ttk.Frame(self.main_frame)
self.control_frame.pack(fill=tk.X, pady=5)
self.settings_btn = ttk.Button(self.control_frame, text="Settings", command=self.open_settings)
self.settings_btn.pack(side=tk.LEFT, padx=5)
# ===== NEW STATS LABEL =====
self.stats_label = ttk.Label(self.main_frame, text="", anchor="w", justify="left")
self.stats_label.pack(fill=tk.X, padx=10, pady=5)
# ===========================
self.canvas_frame = ttk.Frame(self.main_frame)
self.canvas_frame.pack(fill=tk.BOTH, expand=True)
self.fig, (self.ax_bar, self.ax_heat) = plt.subplots(2, 1, figsize=(12, 6),
gridspec_kw={'height_ratios': [1, 3]})
self.canvas = FigureCanvasTkAgg(self.fig, master=self.canvas_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.toolbar = NavigationToolbar2Tk(self.canvas, self.canvas_frame)
self.toolbar.update()
self.canvas._tkcanvas.pack(fill=tk.BOTH, expand=True)
def load_repo(self):
if not self.repo_path:
current_dir = os.getcwd()
try:
git.Repo(current_dir)
self.repo_path = current_dir
except git.InvalidGitRepositoryError:
self.repo_path = filedialog.askdirectory(title="Select Git Repository Folder")
if self.repo_path:
try:
git.Repo(self.repo_path)
self.root.after(100, self.update_graph_with_focus)
except git.InvalidGitRepositoryError:
messagebox.showerror("Error", "No valid Git repository selected.")
self.root.quit()
else:
messagebox.showerror("Error", "No valid Git repository selected.")
self.root.quit()
def update_graph_with_focus(self):
self.update_graph()
self.root.focus_force()
def get_commit_activity(self, email=None, start_date_str=None, end_date_str=None, max_days=365):
try:
repo = git.Repo(self.repo_path)
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() if start_date_str else None
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() if end_date_str else self.end_date
commits = repo.iter_commits()
activity = defaultdict(int)
oldest_date = None
newest_date = None
commit_count = 0
for commit in commits:
date = datetime.fromtimestamp(commit.committed_date).date()
if email and commit.author.email != email:
continue
if date > end_date:
continue
activity[date] += 1
commit_count += 1
if oldest_date is None or date < oldest_date:
oldest_date = date
if newest_date is None or date > newest_date:
newest_date = date
if not activity:
raise ValueError("No commits found in the repository (or matching the filter).")
if start_date is None and oldest_date:
repo_age_days = (end_date - oldest_date).days
calc_start = oldest_date if repo_age_days < max_days else end_date - timedelta(days=max_days)
else:
calc_start = start_date if start_date else end_date - timedelta(days=max_days)
calc_start = max(calc_start, oldest_date) if oldest_date else calc_start
filtered_activity = {d: c for d, c in activity.items() if calc_start <= d <= end_date}
if not getattr(sys, 'frozen', False) and email:
print(f"Commits for {email}: {commit_count}")
return filtered_activity, calc_start
except git.InvalidGitRepositoryError:
raise ValueError("Selected folder is not a valid Git repository.")
except ValueError as e:
raise ValueError(f"Invalid date or data: {str(e)}")
except Exception as e:
raise ValueError(f"Error in commit activity: {str(e)}")
def get_unique_emails(self):
try:
repo = git.Repo(self.repo_path)
emails = set()
for commit in repo.iter_commits(max_count=1000):
emails.add(commit.author.email)
return sorted(list(emails))
except git.InvalidGitRepositoryError:
return []
except Exception:
return []
def update_graph(self):
try:
plt.close(self.fig)
self.fig, (self.ax_bar, self.ax_heat) = plt.subplots(2, 1, figsize=(12, 6),
gridspec_kw={'height_ratios': [1, 3]})
self.canvas.figure = self.fig
self.activity, self.start_date = self.get_commit_activity(self.email,
str(self.start_date)[:10],
str(self.end_date)[:10])
if not self.activity:
messagebox.showinfo("No Data", "No commits found for the selected email/date range. Try a different email or wider dates.")
return
repo_name = os.path.basename(self.repo_path.rstrip(os.sep))
all_dates = pd.date_range(start=self.start_date, end=self.end_date)
first_day_weekday = self.start_date.weekday()
offset = (first_day_weekday + 1) % 7
counts = [0] * offset + [self.activity.get(d.date(), 0) for d in all_dates]
total_days = len(counts)
num_weeks = (total_days + 6) // 7
total_cells = num_weeks * 7
pad_end = total_cells - total_days
counts += [0] * pad_end
data = np.array(counts).reshape(num_weeks, 7).T
weekly_totals = data.sum(axis=0)
# ===== NEW STATS =====
total_commits = sum(self.activity.values())
today = self.end_date
current_day_commits = self.activity.get(today, 0)
weekday = today.weekday()
offset_days = (weekday + 1) % 7
week_start = today - timedelta(days=offset_days)
current_week_commits = sum(
self.activity.get(week_start + timedelta(days=i), 0)
for i in range(7)
)
highest_daily = max(self.activity.values()) if self.activity else 0
highest_weekly = max(weekly_totals) if len(weekly_totals) > 0 else 0
stats_text = (
f"Total Commits: {total_commits} | "
f"Current Week: {current_week_commits} | "
f"Today: {current_day_commits}\n"
f"Peak Week: {highest_weekly} | "
f"Peak Day: {highest_daily}"
)
self.stats_label.config(text=stats_text)
# =====================
x_range = range(len(weekly_totals))
colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39']
cmap = ListedColormap(colors)
max_val = np.max(data)
bounds = [0, 1, 3, 7, 12, max(13, max_val + 1)]
norm = BoundaryNorm(bounds, cmap.N)
midpoints = [(bounds[i] + bounds[i+1]) / 2 for i in range(len(bounds)-1)]
self.ax_bar.bar(x_range, weekly_totals, color='skyblue', edgecolor='black', width=1.0)
self.ax_bar.set_title(f'Weekly Commit Totals - {repo_name}')
self.ax_bar.set_ylabel('Commits')
self.ax_bar.grid(axis='y', linestyle='--', alpha=0.7)
self.ax_bar.set_xlim(-0.5, len(x_range) - 0.5)
im = self.ax_heat.imshow(data, cmap=cmap, norm=norm, aspect='auto')
self.ax_heat.set_yticks(range(7))
self.ax_heat.set_yticklabels(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])
self.ax_heat.yaxis.tick_right()
self.ax_heat.set_xlim(-0.5, len(x_range) - 0.5)
tick_positions = [0]
month_labels = [self.start_date.strftime('%b')]
current_month = self.start_date.month
col = 0
for i in range(offset, total_days, 7):
week_start = self.start_date + timedelta(days=(i - offset))
if week_start.month != current_month:
tick_positions.append(col)
month_labels.append(week_start.strftime('%b'))
current_month = week_start.month
col += 1
self.ax_heat.set_xticks(tick_positions)
self.ax_heat.set_xticklabels(month_labels)
self.ax_bar.set_xticks(tick_positions)
self.ax_bar.set_xticklabels(month_labels, rotation=45, fontsize=8)
self.fig.subplots_adjust(bottom=0.15, top=0.9)
cbar = plt.colorbar(im, ax=self.ax_heat, orientation='horizontal', pad=0.1, ticks=midpoints)
cbar.set_ticklabels(['No commits', '1-2', '3-6', '7-11', '12+'])
self.root.title(f'Git Contribution Graph - {repo_name}')
self.fig.suptitle(f'Git Contribution Calendar - {repo_name}')
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
except ValueError as e:
messagebox.showerror("Error", f"Error updating graph: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Unexpected error updating graph: {str(e)}")
def open_settings(self):
settings_window = tk.Toplevel(self.root)
settings_window.title("Settings")
settings_window.geometry("350x250")
emails = self.get_unique_emails()
if not emails:
messagebox.showwarning("No Emails", "No author emails found in the repository.")
emails = [""]
else:
emails.insert(0, "")
ttk.Label(settings_window, text="Email (select or leave blank for all):").pack(pady=5)
email_var = tk.StringVar(value=self.email or "")
max_length = max(len(email) for email in emails) if emails else 20
email_dropdown = ttk.Combobox(settings_window, textvariable=email_var, values=emails,
state="readonly", width=max(max_length, 30))
email_dropdown.pack(pady=5)
ttk.Label(settings_window, text="Start Date (YYYY-MM-DD, blank for 365 days ago):").pack(pady=5)
start_date_entry = ttk.Entry(settings_window)
start_date_entry.insert(0, str(self.start_date)[:10])
start_date_entry.pack(pady=5)
ttk.Label(settings_window, text="End Date (YYYY-MM-DD, blank for today):").pack(pady=5)
end_date_entry = ttk.Entry(settings_window)
end_date_entry.insert(0, str(self.end_date)[:10])
end_date_entry.pack(pady=5)
def save_settings():
self.email = email_var.get() if email_var.get() else None
start_date = start_date_entry.get() if start_date_entry.get() else None
end_date = end_date_entry.get() if end_date_entry.get() else None
if start_date:
try:
self.start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
except ValueError:
messagebox.showerror("Error", "Invalid start date format. Use YYYY-MM-DD.")
return
if end_date:
try:
self.end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
except ValueError:
messagebox.showerror("Error", "Invalid end date format. Use YYYY-MM-DD.")
return
if self.start_date > self.end_date:
messagebox.showerror("Error", "Start date cannot be after end date.")
return
settings_window.destroy()
self.update_graph_with_focus()
ttk.Button(settings_window, text="Save", command=save_settings).pack(pady=10)
def on_closing(self):
self.root.quit()
def main():
try:
root = tk.Tk()
app = GitGraphApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
except Exception as e:
messagebox.showerror("Error", f"Unexpected error: {str(e)}")
if not getattr(sys, 'frozen', False):
print(f"Error: {str(e)}")
input("Press Enter to exit...")
if __name__ == '__main__':
if getattr(sys, 'frozen', False):
import multiprocessing
multiprocessing.freeze_support()
main()