Skip to content

Commit f97bdaa

Browse files
committed
feat: enhance activity reporting and display
Major improvements to activity tracking and report generation: Commit handling: - Add commit stats (additions/deletions) - Show short SHA with each commit - Deduplicate commits using SHA - Handle fork repositories properly - Skip showing commits if they're in parent repo Pull Request improvements: - Track PR source and target repos - Better PR ownership detection - Group PRs by title - Show PR status with emojis (✅ closed, 🔄 open) Report formatting: - Add summary statistics (total commits/PRs) - Show date range in report header - Format commit stats with colored +/- indicators - Only show first line of commit messages - Improve team report formatting
1 parent f6807ac commit f97bdaa

2 files changed

Lines changed: 193 additions & 75 deletions

File tree

config.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

whatdidyougetdone.py

Lines changed: 193 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,56 @@
1717

1818
import click
1919
from datetime import datetime, timedelta, timezone
20+
import json
2021
import os
2122
from pathlib import Path
2223
from typing import Optional
2324
import sys
2425
import webbrowser
2526
from github import Github
27+
from platformdirs import user_config_dir
28+
29+
# Define app name and author for config directory
30+
APP_NAME = "whatdidyougetdone"
31+
APP_AUTHOR = "brayo"
32+
33+
def get_config_dir() -> Path:
34+
"""Get the platform-specific config directory."""
35+
config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
36+
config_dir.mkdir(parents=True, exist_ok=True)
37+
return config_dir
38+
39+
def get_github_token() -> str:
40+
"""
41+
Get GitHub token from config file or environment.
42+
Updates config file if token is in environment.
43+
Exits if no token is found.
44+
"""
45+
config_file = get_config_dir() / ".env"
46+
47+
# Try reading from config file first
48+
if config_file.exists():
49+
with open(config_file) as f:
50+
for line in f:
51+
if line.startswith('GITHUB_TOKEN='):
52+
return line.split('=')[1].strip()
53+
54+
# Check environment variable
55+
token = os.getenv("GITHUB_TOKEN")
56+
if token:
57+
# Save to config file for future use
58+
with open(config_file, 'w') as f:
59+
f.write(f"GITHUB_TOKEN={token}\n")
60+
return token
61+
62+
# No token found
63+
print("GitHub token not found!")
64+
print("Either:")
65+
print(f"1. Create {config_file} with GITHUB_TOKEN=your_token")
66+
print("2. Set GITHUB_TOKEN environment variable")
67+
print("\nYou can create a token at: https://github.com/settings/tokens")
68+
print("Required scopes: repo, read:user")
69+
exit(1)
2670

2771
# Get script directory
2872
SCRIPT_DIR = Path(__file__).parent.absolute()
@@ -33,7 +77,6 @@ def preview_in_browser(filename: Path):
3377
if click.confirm("Open in browser?"):
3478
webbrowser.open(f"file://{os.path.abspath(filename)}")
3579

36-
from config import get_github_token
3780

3881
def setup_github():
3982
"""Ensure GitHub token is available."""
@@ -58,25 +101,81 @@ def get_user_activity(username: str, days: int = 7):
58101

59102
# Format based on event type
60103
if event.type == "PushEvent":
61-
for commit in event.payload["commits"]:
62-
activities.append(
63-
{
64-
"type": "commit",
65-
"repo": event.repo.name,
66-
"message": commit["message"],
67-
"date": event.created_at,
68-
}
69-
)
104+
# Only process commits if:
105+
# 1. The push was by the user we're interested in
106+
# 2. If it's a fork, only include commits not from parent repo
107+
if event.actor.login == username:
108+
# Get repo info to check if it's a fork
109+
repo_info = json.loads(os.popen(f'gh repo view {event.repo.name} --json isFork,parent').read())
110+
111+
# For forks, skip showing commits if they're in the parent
112+
if repo_info.get('isFork', False):
113+
parent_repo = repo_info['parent']
114+
parent_name = f"{parent_repo['owner']['login']}/{parent_repo['name']}"
115+
116+
# If user has access to parent repo, show commits there instead
117+
try:
118+
os.popen(f'gh repo view {parent_name}').read()
119+
# User has access, skip showing in fork
120+
continue
121+
except:
122+
# User doesn't have access to parent, show in fork
123+
for commit in event.payload["commits"]:
124+
activities.append({
125+
"type": "commit",
126+
"repo": event.repo.name,
127+
"message": commit["message"],
128+
"date": event.created_at,
129+
"author": commit["author"],
130+
"sha": commit["sha"]
131+
})
132+
else:
133+
# For non-forks, include all commits
134+
for commit in event.payload["commits"]:
135+
# Get commit stats from GitHub API
136+
try:
137+
stats_output = os.popen(f'gh api repos/{event.repo.name}/commits/{commit["sha"]}').read()
138+
commit_data = json.loads(stats_output)
139+
activities.append({
140+
"type": "commit",
141+
"repo": event.repo.name,
142+
"message": commit["message"],
143+
"date": event.created_at,
144+
"author": commit["author"],
145+
"sha": commit["sha"],
146+
"stats": commit_data.get("stats", {})
147+
})
148+
except:
149+
# If we can't get stats, add commit without them
150+
activities.append({
151+
"type": "commit",
152+
"repo": event.repo.name,
153+
"message": commit["message"],
154+
"date": event.created_at,
155+
"author": commit["author"],
156+
"sha": commit["sha"]
157+
})
70158
elif event.type == "PullRequestEvent":
71-
activities.append(
72-
{
159+
pr = event.payload["pull_request"]
160+
base_repo = pr["base"]["repo"]["full_name"]
161+
head_repo = pr["head"]["repo"]["full_name"]
162+
163+
# Include PR if:
164+
# 1. User created the PR (regardless of target)
165+
# 2. PR is targeting user's repo
166+
pr_author = pr["user"]["login"]
167+
base_owner = pr["base"]["repo"]["owner"]["login"]
168+
169+
if pr_author == username or base_owner == username:
170+
activities.append({
73171
"type": "pr",
74-
"repo": event.repo.name,
75-
"title": event.payload["pull_request"]["title"],
76-
"state": event.payload["pull_request"]["state"],
172+
"repo": base_repo,
173+
"title": pr["title"],
174+
"state": pr["state"],
77175
"date": event.created_at,
78-
}
79-
)
176+
"head_repo": head_repo,
177+
"author": pr_author
178+
})
80179
# Add more event types as needed
81180

82181
return activities
@@ -93,28 +192,91 @@ def generate_report(username: str, days: int = 7):
93192
if repo not in repos:
94193
repos[repo] = []
95194
repos[repo].append(activity)
96-
195+
196+
# Calculate date range
197+
end_date = datetime.now(timezone.utc)
198+
start_date = end_date - timedelta(days=days)
199+
97200
# Generate markdown
98201
report = f"# What did {username} get done?\n\n"
99-
report += f"Activity report for the last {days} days:\n\n"
100-
101-
for repo, acts in repos.items():
102-
report += f"## {repo}\n\n"
202+
report += f"Activity from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}:\n\n"
203+
204+
# Add summary if there's activity
205+
if activities:
206+
total_commits = len({a["sha"] for a in activities if a["type"] == "commit"})
207+
total_prs = len({(a["repo"], a["title"]) for a in activities if a["type"] == "pr"})
208+
report += f"Summary:\n"
209+
report += f"- 💻 {total_commits} unique commits\n"
210+
report += f"- 🔀 {total_prs} pull requests\n\n"
211+
else:
212+
report += "No activity found in this time period.\n\n"
213+
214+
# Sort repos by activity date
215+
sorted_repos = sorted(
216+
repos.items(),
217+
key=lambda x: max((act["date"] for act in x[1])),
218+
reverse=True
219+
)
220+
221+
for repo, acts in sorted_repos:
222+
# Get repo info
223+
repo_info = json.loads(os.popen(f'gh repo view {repo} --json isFork,parent').read())
224+
225+
# Format repo header
226+
if repo_info.get('isFork', False):
227+
parent = repo_info['parent']
228+
report += f"## {repo} (fork of {parent['owner']['login']}/{parent['name']})\n\n"
229+
else:
230+
report += f"## {repo}\n\n"
231+
103232
# Group by type
104233
commits = [act for act in acts if act["type"] == "commit"]
105234
prs = [act for act in acts if act["type"] == "pr"]
106-
235+
236+
# Skip PRs in fork if they target parent
237+
if repo_info.get('isFork', False):
238+
prs = [pr for pr in prs if pr["head_repo"] == repo]
239+
240+
# Group PRs by title
241+
pr_groups = {}
242+
for pr in prs:
243+
title = pr['title']
244+
if title not in pr_groups:
245+
pr_groups[title] = {'date': pr['date'], 'states': set()}
246+
pr_groups[title]['states'].add(pr['state'])
247+
# Keep the most recent date
248+
if pr['date'] > pr_groups[title]['date']:
249+
pr_groups[title]['date'] = pr['date']
250+
107251
# Show PRs first (higher level changes)
108-
for act in sorted(prs, key=lambda x: x["date"], reverse=True):
109-
report += f"- 🔀 {act['title']} ({act['state']})\n"
110-
111-
# Then show commits
252+
for title, info in sorted(pr_groups.items(), key=lambda x: x[1]['date'], reverse=True):
253+
states = info['states']
254+
status = "✅" if "closed" in states else "🔄"
255+
report += f"- {status} {title}\n"
256+
257+
# Then show commits (deduplicated by SHA)
258+
seen_shas = set()
112259
for act in sorted(commits, key=lambda x: x["date"], reverse=True):
113260
# Skip merge commits and commits that are part of PRs
114261
if act["message"].startswith("Merge") or "Co-authored-by" in act["message"]:
115262
continue
116-
report += f"- 💻 {act['message']}\n"
117-
263+
264+
if act["sha"] not in seen_shas:
265+
seen_shas.add(act["sha"])
266+
# Get first line of commit message for display
267+
message = act["message"].split('\n')[0].strip()
268+
short_sha = act["sha"][:7] # First 7 chars of SHA
269+
270+
# Format additions/deletions stats
271+
stats = ""
272+
if "stats" in act:
273+
adds = act["stats"].get("additions", 0)
274+
dels = act["stats"].get("deletions", 0)
275+
if adds or dels:
276+
stats = f"<span style=\"color: #28a745\">+{adds}</span><span style=\"color: #dc3545\">-{dels}</span>"
277+
278+
report += f"- 💻 {message} ({short_sha}){stats}\n"
279+
118280
return report
119281

120282
def save_report(username: str, report: str) -> Path:
@@ -181,7 +343,8 @@ def team(usernames: tuple[str], days: int):
181343
if activity["type"] == "commit":
182344
report += f"- [{activity['repo']}] {activity['message']}\n"
183345
elif activity["type"] == "pr":
184-
report += f"- [{activity['repo']}] {activity['title']} ({activity['state']})\n"
346+
status = "✅" if activity["state"] == "closed" else "🔄"
347+
report += f"- [{activity['repo']}] {status} {activity['title']}\n"
185348
report += "\n"
186349

187350
# Save report

0 commit comments

Comments
 (0)