1919import os
2020from pathlib import Path
2121from typing import Optional
22- import sys
23- import webbrowser
2422from github import Github
2523
2624# Get script directory
2725SCRIPT_DIR = Path (__file__ ).parent .absolute ()
2826
2927
30- def preview_in_browser (filename : Path ) -> None :
31- """Open file in browser if in interactive mode."""
32- if sys .stdout .isatty ():
33- if click .confirm ("Open in browser?" ):
34- webbrowser .open (f"file://{ filename .absolute ()} " )
35-
36-
3728def get_github_token () -> str :
3829 """Get GitHub token from environment or GitHub CLI."""
3930 # Try environment variable first
@@ -106,10 +97,25 @@ def get_user_activity(username: str, days: int = 7):
10697 return activities
10798
10899
109- def generate_report (username : str , days : int = 7 ):
100+ def generate_report (username : str , days : int = 7 , include_timeline : bool = False ):
110101 """Generate a markdown report of user activity."""
111102 activities = get_user_activity (username , days )
112103
104+ # Calculate summary stats
105+ total_commits = sum (1 for a in activities if a ["type" ] == "commit" )
106+ total_prs = sum (1 for a in activities if a ["type" ] == "pr" )
107+ active_repos = len ({a ["repo" ] for a in activities })
108+
109+ # Generate markdown
110+ report = f"# What did { username } get done?\n \n "
111+ report += f"Activity report for the last { days } days:\n \n "
112+
113+ # Summary stats
114+ report += "## Summary\n \n "
115+ report += f"- 💻 { total_commits } commits\n "
116+ report += f"- 🔀 { total_prs } pull requests\n "
117+ report += f"- 📦 { active_repos } active repositories\n \n "
118+
113119 # Group by repo
114120 repos : dict [str , list [dict ]] = {}
115121 for activity in activities :
@@ -118,12 +124,10 @@ def generate_report(username: str, days: int = 7):
118124 repos [repo ] = []
119125 repos [repo ].append (activity )
120126
121- # Generate markdown
122- report = f"# What did { username } get done?\n \n "
123- report += f"Activity report for the last { days } days:\n \n "
124-
127+ # Activity by repo
128+ report += "## Activity by Repository\n \n "
125129 for repo , acts in repos .items ():
126- report += f"## { repo } \n \n "
130+ report += f"### { repo } \n \n "
127131 # Group by type
128132 commits = [act for act in acts if act ["type" ] == "commit" ]
129133 prs = [act for act in acts if act ["type" ] == "pr" ]
@@ -137,21 +141,27 @@ def generate_report(username: str, days: int = 7):
137141 # Skip merge commits and commits that are part of PRs
138142 if act ["message" ].startswith ("Merge" ) or "Co-authored-by" in act ["message" ]:
139143 continue
140- report += f"- 💻 { act ['message' ]} \n "
144+ # Take only first line of commit message
145+ message = act ["message" ].split ("\n " )[0 ]
146+ report += f"- 💻 { message } \n "
147+
148+ # Optional timeline
149+ if include_timeline :
150+ report += "\n <details><summary>Detailed Timeline</summary>\n \n "
151+ for act in sorted (activities , key = lambda x : x ["date" ], reverse = True ):
152+ date_str = act ["date" ].strftime ("%Y-%m-%d %H:%M" )
153+ if act ["type" ] == "commit" :
154+ message = act ["message" ].split ("\n " )[0 ]
155+ report += f"- { date_str } 💻 [{ act ['repo' ]} ] { message } \n "
156+ elif act ["type" ] == "pr" :
157+ report += (
158+ f"- { date_str } 🔀 [{ act ['repo' ]} ] { act ['title' ]} ({ act ['state' ]} )\n "
159+ )
160+ report += "\n </details>\n "
141161
142162 return report
143163
144164
145- def save_report (username : str , report : str ) -> Path :
146- """Save report to file."""
147- reports_dir = SCRIPT_DIR / "reports"
148- reports_dir .mkdir (exist_ok = True )
149-
150- filename = reports_dir / f"{ username } -{ datetime .now ().strftime ('%Y-%m-%d' )} .md"
151- filename .write_text (report )
152- return filename
153-
154-
155165@click .group ()
156166def cli ():
157167 """What did you get done? - Activity report generator"""
@@ -161,63 +171,125 @@ def cli():
161171@cli .command ()
162172@click .argument ("username" )
163173@click .option ("--days" , default = 7 , help = "Number of days to look back" )
164- @click .option ("--output" , help = "Output file (default: reports/<username>-<date>.md)" )
165- def report (username : str , days : int , output : Optional [str ]):
174+ @click .option ("--file" , help = "Save output to file instead of stdout" )
175+ @click .option ("--timeline" , is_flag = True , help = "Include detailed timeline" )
176+ def report (username : str , days : int , file : Optional [str ], timeline : bool ):
166177 """Generate activity report for a GitHub user"""
167178 setup_github ()
168179
169180 # Generate report
170- report_text = generate_report (username , days )
181+ report_text = generate_report (username , days , include_timeline = timeline )
171182
172- # Save report
173- if output :
174- filename = Path (output )
183+ # Output report
184+ if file :
185+ Path (file ).write_text (report_text )
186+ print (f"Report saved to: { file } " )
175187 else :
176- filename = save_report (username , report_text )
177-
178- print (f"Report saved to: { filename } " )
179- preview_in_browser (filename )
188+ print (report_text )
180189
181190
182191@cli .command ()
183192@click .argument ("usernames" , nargs = - 1 )
184193@click .option ("--days" , default = 7 , help = "Number of days to look back" )
185- def team (usernames : tuple [str ], days : int ):
194+ @click .option ("--file" , help = "Save output to file instead of stdout" )
195+ @click .option ("--timeline" , is_flag = True , help = "Include detailed timeline" )
196+ def team (usernames : tuple [str ], days : int , file : Optional [str ], timeline : bool ):
186197 """Generate team activity report"""
187198 setup_github ()
188199
189200 # Generate combined report
190201 report = "# Team Activity Report\n \n "
191202 report += f"Activity for the last { days } days\n \n "
192203
204+ # Team summary
205+ total_team_commits = 0
206+ total_team_prs = 0
207+ active_repos : set [str ] = set ()
208+
209+ # Collect all activities first
210+ all_activities : list [tuple [str , dict ]] = []
193211 for username in usernames :
194212 activities = get_user_activity (username , days )
195- report += f"## { username } \n \n "
213+ all_activities . extend (( username , a ) for a in activities )
196214
197- # Count activities
215+ # Update team stats
198216 commit_count = sum (1 for a in activities if a ["type" ] == "commit" )
199217 pr_count = sum (1 for a in activities if a ["type" ] == "pr" )
218+ active_repos .update (a ["repo" ] for a in activities )
200219
201- report += f"- 💻 { commit_count } commits \n "
202- report += f"- 🔀 { pr_count } pull requests \n \n "
220+ total_team_commits += commit_count
221+ total_team_prs += pr_count
203222
204- # Add details
205- for activity in sorted (activities , key = lambda x : x ["date" ], reverse = True ):
206- if activity ["type" ] == "commit" :
207- report += f"- [{ activity ['repo' ]} ] { activity ['message' ]} \n "
208- elif activity ["type" ] == "pr" :
209- report += f"- [{ activity ['repo' ]} ] { activity ['title' ]} ({ activity ['state' ]} )\n "
210- report += "\n "
223+ # Team summary
224+ report += "## Team Summary\n \n "
225+ report += f"- 👥 { len (usernames )} team members\n "
226+ report += f"- 💻 { total_team_commits } commits\n "
227+ report += f"- 🔀 { total_team_prs } pull requests\n "
228+ report += f"- 📦 { len (active_repos )} active repositories\n \n "
211229
212- # Save report
213- reports_dir = SCRIPT_DIR / "reports"
214- reports_dir .mkdir (exist_ok = True )
230+ # Per-user summary
231+ for username in usernames :
232+ user_activities = [a for u , a in all_activities if u == username ]
233+ report += f"## { username } \n \n "
215234
216- filename = reports_dir / f"team-{ datetime .now ().strftime ('%Y-%m-%d' )} .md"
217- filename .write_text (report )
235+ # User stats
236+ commit_count = sum (1 for a in user_activities if a ["type" ] == "commit" )
237+ pr_count = sum (1 for a in user_activities if a ["type" ] == "pr" )
238+ user_repos = len ({a ["repo" ] for a in user_activities })
218239
219- print (f"Team report saved to: { filename } " )
220- preview_in_browser (filename )
240+ report += f"- 💻 { commit_count } commits\n "
241+ report += f"- 🔀 { pr_count } pull requests\n "
242+ report += f"- 📦 { user_repos } active repositories\n \n "
243+
244+ # Group by repo
245+ repos : dict [str , list [dict ]] = {}
246+ for activity in user_activities :
247+ repo = activity ["repo" ]
248+ if repo not in repos :
249+ repos [repo ] = []
250+ repos [repo ].append (activity )
251+
252+ for repo , acts in repos .items ():
253+ report += f"### { repo } \n \n "
254+ # Group by type
255+ commits = [act for act in acts if act ["type" ] == "commit" ]
256+ prs = [act for act in acts if act ["type" ] == "pr" ]
257+
258+ # Show PRs first
259+ for act in sorted (prs , key = lambda x : x ["date" ], reverse = True ):
260+ report += f"- 🔀 { act ['title' ]} ({ act ['state' ]} )\n "
261+
262+ # Then commits
263+ for act in sorted (commits , key = lambda x : x ["date" ], reverse = True ):
264+ if (
265+ act ["message" ].startswith ("Merge" )
266+ or "Co-authored-by" in act ["message" ]
267+ ):
268+ continue
269+ message = act ["message" ].split ("\n " )[0 ]
270+ report += f"- 💻 { message } \n "
271+ report += "\n "
272+
273+ # Optional timeline
274+ if timeline :
275+ report += "\n <details><summary>Team Timeline</summary>\n \n "
276+ for username , act in sorted (
277+ all_activities , key = lambda x : x [1 ]["date" ], reverse = True
278+ ):
279+ date_str = act ["date" ].strftime ("%Y-%m-%d %H:%M" )
280+ if act ["type" ] == "commit" :
281+ message = act ["message" ].split ("\n " )[0 ]
282+ report += f"- { date_str } 💻 { username } [{ act ['repo' ]} ] { message } \n "
283+ elif act ["type" ] == "pr" :
284+ report += f"- { date_str } 🔀 { username } [{ act ['repo' ]} ] { act ['title' ]} ({ act ['state' ]} )\n "
285+ report += "\n </details>\n "
286+
287+ # Output report
288+ if file :
289+ Path (file ).write_text (report )
290+ print (f"Report saved to: { file } " )
291+ else :
292+ print (report )
221293
222294
223295if __name__ == "__main__" :
0 commit comments