| 
									
										
										
										
											2025-05-07 17:09:41 +08:00
										 |  |  | 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 | 
					
						
							| 
									
										
										
										
											2025-05-07 17:10:24 +08:00
										 |  |  |         print(f"{email}: {lines}/{total_lines} {percent:.2f}%") | 
					
						
							| 
									
										
										
										
											2025-05-07 17:09:41 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     main() |