#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-or-later """ Example use: credits_git_gen.py --source=/src/blender --range=SHA1..HEAD """ from git_log import GitCommitIter import unicodedata as ud # ----------------------------------------------------------------------------- # Lookup Table to clean up the credits # # This is a combination of unifying git logs as well as # name change requested by the authors. author_table = { "Aaron": "Aaron Carlisle", "Your Name": "Aaron Carlisle", "Alan": "Alan Troth", "andreas atteneder": "Andreas Atteneder", "Ankit": "Ankit Meel", "Antonioya": "Antonio Vazquez", "Antonio Vazquez": "Antonio Vazquez", "Antony Ryakiotakis": "Antony Riakiotakis", "Amélie Fondevilla": "Amelie Fondevilla", "bastien": "Bastien Montagne", "mont29": "Bastien Montagne", "bjornmose": "Bjorn Mose", "meta-androcto": "Brendon Murphy", "Brecht van Lommel": "Brecht Van Lommel", "Brecht Van Lömmel": "Brecht Van Lommel", "recht Van Lommel": "Brecht Van Lommel", "Clément Foucault": "Clément Foucault", "Clément": "Clément Foucault", "fclem": "Clément Foucault", "Clment Foucault": "Clément Foucault", "christian brinkmann": "Christian Brinkmann", "ZanQdo": "Daniel Salazar", "unclezeiv": "Davide Vercelli", "dilithjay": "Dilith Jayakody", "gaiaclary": "Gaia Clary", "Diego Hernan Borghetti": "Diego Borghetti", "Dotsnov Valentin": "Dontsov Valentin", "Eitan": "Eitan Traurig", "EitanSomething": "Eitan Traurig", "Erik": "Erik Abrahamsson", "Erick Abrahammson": "Erik Abrahamsson", "Eric Abrahamsson": "Erik Abrahamsson", "Ethan-Hall": "Ethan Hall", "filedescriptor": "Falk David", "Germano": "Germano Cavalcante", "Germano Cavalcantemano-wii": "Germano Cavalcante", "mano-wii": "Germano Cavalcante", "gsr": "Guillermo S. Romero", "Henrik Dick (weasel)": "Henrik Dick", "howardt": "Howard Trickey", "Iliay Katueshenock": "Iliya Katueshenock", "MOD": "Iliya Katueshenock", "Inês Almeida": "Ines Almeida", "brita": "Ines Almeida", "Ivan": "Ivan Perevala", "jensverwiebe": "Jens Verwiebe", "Jesse Y": "Jesse Yurkovich", "Joe Eagar": "Joseph Eagar", "Johnny Matthews (guitargeek)": "Johnny Matthews", "guitargeek": "Johnny Matthews", "jon denning": "Jon Denning", "julianeisel": "Julian Eisel", "Severin": "Julian Eisel", "Alex Strand": "Kenzie Strand", "Kevin Dietrich": "Kévin Dietrich", "Leon Leno": "Leon Schittek", "Lukas Toenne": "Lukas Tönne", "Mikhail": "Mikhail Matrosov", "OmarSquircleArt": "Omar Emara", "lazydodo": "Ray Molenkamp", "Ray molenkamp": "Ray Molenkamp", "Author Name": "Robert Guetzkow", "Sybren A. Stüvel": "Sybren A. Stüvel", "Simon": "Simon G", "Stephan": "Stephan Seitz", "Sebastian Herhoz": "Sebastian Herholz", "blender": "Sergey Sharybin", "Vuk GardaÅ¡ević": "Vuk Gardašević", "ianwill": "Willian Padovani Germano", "Yiming Wu": "YimingWu", } # ----------------------------------------------------------------------------- # Class for generating credits class CreditUser: __slots__ = ( "commit_total", "year_min", "year_max", ) def __init__(self): self.commit_total = 0 class Credits: __slots__ = ( "users", ) def __init__(self): self.users = {} def process_commit(self, c): # Normalize author string into canonical form, prevents duplicate credit users author = ud.normalize('NFC', c.author) author = author_table.get(author, author) year = c.date.year cu = self.users.get(author) if cu is None: cu = self.users[author] = CreditUser() cu.year_min = year cu.year_max = year cu.commit_total += 1 cu.year_min = min(cu.year_min, year) cu.year_max = max(cu.year_max, year) def process(self, commit_iter): for i, c in enumerate(commit_iter): self.process_commit(c) if not (i % 100): print(i) def write(self, filepath, is_main_credits=True, contrib_companies=(), sort="name"): # patch_word = "patch", "patches" commit_word = "commit", "commits" sorted_authors = {} if sort == "commit": sorted_authors = dict(sorted(self.users.items(), key=lambda item: item[1].commit_total)) else: sorted_authors = dict(sorted(self.users.items())) with open(filepath, 'w', encoding="ascii", errors='xmlcharrefreplace') as file: file.write("

Individual Contributors

\n\n") for author, cu in sorted_authors.items(): file.write("{:s}, {:,d} {:s} {:s}
\n".format( author, cu.commit_total, commit_word[cu.commit_total > 1], ("" if not is_main_credits else ("- {:d}".format(cu.year_min) if cu.year_min == cu.year_max else ("({:d} - {:d})".format(cu.year_min, cu.year_max)))))) file.write("\n\n") # ------------------------------------------------------------------------- # Companies, hard coded if is_main_credits: file.write("

Contributions from Companies & Organizations

\n") file.write("

\n") for line in contrib_companies: file.write("{:s}
\n".format(line)) file.write("

\n") import datetime now = datetime.datetime.now() fn = __file__.split("\\")[-1].split("/")[-1] file.write( "

Generated by '{:s}' {:d}/{:d}/{:d}

\n".format( fn, now.year, now.month, now.day )) def argparse_create(): import argparse # When --help or no args are given, print this help usage_text = "Review revisions." epilog = "This script is used to generate credits" parser = argparse.ArgumentParser(description=usage_text, epilog=epilog) parser.add_argument( "--source", dest="source_dir", metavar='PATH', required=True, help="Path to git repository", ) parser.add_argument( "--range", dest="range_sha1", metavar='SHA1_RANGE', required=True, help="Range to use, eg: 169c95b8..HEAD", ) parser.add_argument( "--sort", dest="sort", metavar='METHOD', required=False, help="Sort credits by 'name' (default) or 'commit'", ) return parser def main(): # ---------- # Parse Args args = argparse_create().parse_args() def is_credit_commit_valid(c): ignore_dir = ( b"blender/extern/", b"blender/intern/opennl/", ) if not any(f for f in c.files if not f.startswith(ignore_dir)): return False return True # TODO, there are for sure more companies then are currently listed. # 1 liners for in html syntax contrib_companies = ( "Unity Technologies - FBX Exporter", "BioSkill GmbH - H3D compatibility for X3D Exporter, " "OBJ Nurbs Import/Export", "AutoCRC - Improvements to fluid particles, vertex color baking", "Adidas - Principled BSDF shader in Cycles", "AMD - Cycles HIP GPU rendering, CPU optimizations", "Intel - Cycles oneAPI GPU rendering, CPU optimizations", "NVIDIA - Cycles OptiX GPU rendering, USD importer", "Facebook - Cycles subsurface scattering improvements", "Apple - Cycles Metal GPU backend", ) credits = Credits() # commit_range = "HEAD~10..HEAD" # commit_range = "blender-v2.81-release..blender-v2.82-release" # commit_range = "blender-v2.82-release" commit_range = args.range_sha1 sort = args.sort citer = GitCommitIter(args.source_dir, commit_range) credits.process((c for c in citer if is_credit_commit_valid(c))) credits.write("credits.html", is_main_credits=True, contrib_companies=contrib_companies, sort=sort) print("Written: credits.html") if __name__ == "__main__": main()