import os import subprocess from collections import Counter CONFIG_FILE_EXTENSIONS = ('.json', '.yml', '.yaml', '.ini', '.conf', '.toml') def is_text_file(filepath): # Check for binary file by scanning for null bytes. try: with open(filepath, 'rb') as f: chunk = f.read(4096) if b'\0' in chunk: return False return True except Exception: return False def should_skip_file(path): base = os.path.basename(path) # Skip dotfiles and dotdirs if base.startswith('.'): return True # Skip config files by extension if base.lower().endswith(CONFIG_FILE_EXTENSIONS): return True return False def get_tracked_files(): try: output = subprocess.check_output(['git', 'ls-files'], text=True) files = output.strip().split('\n') files = [f for f in files if f and os.path.isfile(f)] return files except subprocess.CalledProcessError: print('Error: Are you in a git repository?') return [] def main(): files = get_tracked_files() email_counter = Counter() total_lines = 0 for file in files: if should_skip_file(file): continue if not is_text_file(file): continue try: blame = subprocess.check_output(['git', 'blame', '-e', file], text=True, errors='replace') for line in blame.splitlines(): # The email always inside <> if '<' in line and '>' in line: try: email = line.split('<')[1].split('>')[0].strip() except Exception: continue email_counter[email] += 1 total_lines += 1 except subprocess.CalledProcessError: continue for email, lines in email_counter.most_common(): percent = (lines / total_lines * 100) if total_lines else 0 print(f'{email}: {lines}/{total_lines} {percent:.2f}%') if __name__ == '__main__': main()