From 3d4fb68172a10ad368ef788469854ce20f282a2d Mon Sep 17 00:00:00 2001 From: josix Date: Thu, 16 Jan 2025 22:27:18 +0800 Subject: [PATCH] feat: Add --stats option to show code changes and aider contribution statistics --- aider/args.py | 7 ++++ aider/commands.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++ aider/main.py | 5 +++ 3 files changed, 113 insertions(+) diff --git a/aider/args.py b/aider/args.py index 08c9bde76..1f2e65df5 100644 --- a/aider/args.py +++ b/aider/args.py @@ -553,6 +553,13 @@ def get_parser(default_config_files, git_root): help="Run tests, fix problems found and then exit", default=False, ) + group.add_argument( + "--stats", + metavar="REVISIONS", + nargs="?", + const="", + help="Show code changes statistics between revisions", + ) ########## group = parser.add_argument_group("Analytics") diff --git a/aider/commands.py b/aider/commands.py index aaf6d7ddd..ee2ac7632 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1484,6 +1484,107 @@ class Commands: "Toggle multiline mode (swaps behavior of Enter and Meta+Enter)" self.io.toggle_multiline_mode() + def cmd_stats(self, args): + """Show statistics about code changes and aider's contributions by counting lines of code through git blame. + + Usage: + /stats Compare against main/master branch + /stats Compare against specific revision + /stats rev1..rev2 Compare between two specific revisions + + Examples: + /stats Show stats vs main/master branch + /stats HEAD~5 Show stats vs 5 commits ago + /stats v1.0.0 Show stats vs version 1.0.0 + /stats main..HEAD Show stats between main and current HEAD + + Lines are attributed to aider when the git author or committer contains "(aider)". + Binary files (images, audio, etc.) are excluded from the analysis. + """ + if not self.coder.repo: + self.io.tool_error("No git repository found.") + return + + try: + # Get the revision range + if not args: + # Default to comparing against main/master branch + for default_branch in ["main", "master"]: + try: + self.coder.repo.repo.rev_parse(default_branch) + args = default_branch + break + except: + continue + if not args: + self.io.tool_error("No main or master branch found. Please specify a revision.") + return + source_revision, target_revision = args.split("..") if ".." in args else (args, "HEAD") + + # Get files changed between revisions + diff_files = self.coder.repo.repo.git.diff( + "--name-only", f"{source_revision}..{target_revision}" + ).splitlines() + # Filter out media files + files = [f for f in diff_files if not any(f.lower().endswith(ext) for ext in ( + '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.ico', '.svg', # images + '.mp3', '.wav', '.ogg', '.m4a', '.flac', # audio + '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', # video + '.pdf', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', # documents + '.zip', '.tar', '.gz', '.7z', '.rar', # archives + '.ttf', '.otf', '.woff', '.woff2', '.eot' # fonts + ))] + self.io.tool_output(f"Found {len(files)} non-binary tracked files in the repository.") + + total_lines = 0 + aider_lines = 0 + + for file in files: + try: + # Run git blame for each file + blame_output = self.coder.repo.repo.git.blame( + f"{source_revision}..{target_revision}", "-M", "-C", "--line-porcelain", "--", file + ) + + # Parse blame output + for line in blame_output.split('filename'): + total_lines += 1 + for field in line.split('\n'): + # Check author and committer lines for aider attribution + author_match = False + committer_match = False + if field.startswith("author ") or field.startswith("committer "): + author_match = "(aider)" in field.lower() + committer_match = "(aider)" in field.lower() + if author_match or committer_match: + aider_lines += 1 + + except Exception as e: + if "no such path" not in str(e).lower(): + self.io.tool_error(f"Error processing {file}: {e}") + + # Calculate percentages + if total_lines > 0: + aider_percentage = (aider_lines / total_lines) * 100 + human_lines = total_lines - aider_lines + human_percentage = (human_lines / total_lines) * 100 + + # Display results + self.io.tool_output("\nCode contribution statistics:") + self.io.tool_output(f"Total lines of code: {total_lines:,}") + self.io.tool_output( + f"Human-written code: {human_lines:,} lines ({human_percentage:.1f}%)" + ) + self.io.tool_output( + f"Aider-written code: {aider_lines:,} lines ({aider_percentage:.1f}%)" + ) + else: + self.io.tool_output("No lines of code found in the repository.") + + except Exception as e: + self.io.tool_error(f"Error analyzing aider statistics: {e}") + + def cmd_copy(self, args): "Copy the last assistant message to the clipboard" all_messages = self.coder.done_messages + self.coder.cur_messages diff --git a/aider/main.py b/aider/main.py index ea344f0ba..8ae9693b9 100644 --- a/aider/main.py +++ b/aider/main.py @@ -1142,6 +1142,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F analytics.event("exit", reason="Completed --message-file") return + if args.stats: + commands.cmd_stats(args.stats) + analytics.event("exit", reason="Completed --stats") + return + if args.exit: analytics.event("exit", reason="Exit flag set") return