Blender V2.61 - r43446

credits_svn_gen.py

Go to the documentation of this file.
00001 # ##### BEGIN GPL LICENSE BLOCK #####
00002 #
00003 #  This program is free software; you can redistribute it and/or
00004 #  modify it under the terms of the GNU General Public License
00005 #  as published by the Free Software Foundation; either version 2
00006 #  of the License, or (at your option) any later version.
00007 #
00008 #  This program is distributed in the hope that it will be useful,
00009 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 #  GNU General Public License for more details.
00012 #
00013 #  You should have received a copy of the GNU General Public License
00014 #  along with this program; if not, write to the Free Software Foundation,
00015 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
00016 #
00017 # ##### END GPL LICENSE BLOCK #####
00018 
00019 # <pep8 compliant>
00020 
00021 """
00022 This script generates a credits list for:
00023 
00024    http://www.blender.org/development/credits
00025 
00026 
00027 To use this script you'll need to set 2 variables (below)
00028 
00029 eg:
00030 
00031    svn_log = "somelog.xml"
00032    tracker_csv = "tracker_report-2011-09-02.csv"
00033 
00034 
00035 The first is the result of running this:
00036 
00037    svn log https://svn.blender.org/svnroot/bf-blender/trunk/blender -v --xml
00038 
00039 The csv file must be saved from the tracker, be sure to select all patches
00040 not just open ones.
00041 
00042 
00043 Running this script will create a file called 'credits.html',
00044 the resulting data is then be copied into the Development/Credits page
00045 in blender.org's typo3.
00046 """
00047 
00048 # -----------------------------------------------------------------------------
00049 # Generic Class and parsing code, could be useful for all sorts of cases
00050 
00051 
00052 class SvnCommit(object):
00053     """Just data store really"""
00054     __slots__ = ("revision",
00055                  "author",
00056                  "date",
00057                  "message",
00058                  "paths",
00059                  )
00060 
00061     def __init__(self, xml):
00062         self.revision = int(xml.attributes["revision"].nodeValue)
00063 
00064         elems = xml.getElementsByTagName("author")
00065         self.author = elems[0].firstChild.nodeValue
00066 
00067         elems = xml.getElementsByTagName("date")
00068         self.date = elems[0].firstChild.nodeValue
00069 
00070         # treat the message
00071         # possible there is no message
00072         elems = xml.getElementsByTagName("msg")
00073         message = getattr(elems[0].firstChild, "nodeValue", "")
00074         message = " ".join(message.split())
00075         self.message = message
00076 
00077         # for now we ignore: elem.attributes["kind"]
00078         self.paths = [(elem.attributes["action"].value,
00079                        elem.firstChild.nodeValue,
00080                        )
00081                       for elem in xml.getElementsByTagName("path")]
00082 
00083     def __repr__(self):
00084         repr_dict = {}
00085         for attr in self.__slots__:
00086             repr_dict[attr] = getattr(self, attr)
00087         return repr(repr_dict)
00088 
00089 
00090 def parse_commits(filepath):
00091     from xml.dom.minidom import parse
00092 
00093     svn_xml = parse(filepath)
00094     # almost certainly only 1 but, just incase
00095 
00096     commits = []
00097 
00098     for log_list in svn_xml.getElementsByTagName("log"):
00099         log_entries = log_list.getElementsByTagName("logentry")
00100         for commit_xml in log_entries:
00101 
00102             # get all data from the commit into a dict for more easy checking.
00103             commits.append(SvnCommit(commit_xml))
00104 
00105     return commits
00106 
00107 
00108 # -----------------------------------------------------------------------------
00109 # Special checks to extract the credits
00110 
00111 #svn_log = "/dsk/data/src/blender/svn_log_verbose.xml"
00112 svn_log = "/dsk/data/src/blender/svn_log_verbose.xml"
00113 tracker_csv = "/l/tracker_report-2011-10-17.csv"
00114 
00115 # TODO, there are for sure more companies then are currently listed.
00116 # 1 liners for in wiki syntax
00117 contrib_companies = [
00118     "<b>Unity Technologies</b> - FBX Exporter",
00119     "<b>BioSkill GmbH</b> - H3D compatibility for X3D Exporter, "
00120     "OBJ Nurbs Import/Export",
00121     "<b>AutoCRC</b> - Improvements to fluid particles",
00122 ]
00123 
00124 # ignore commits containing these messages
00125 ignore_msg = (
00126     "SVN maintenance",
00127     )
00128 
00129 # ignore these paths
00130 # implicitly ignore anything _not_ in /trunk/blender
00131 ignore_dir = (
00132     "/trunk/blender/extern/",
00133     "/trunk/blender/scons/",
00134     "/trunk/blender/intern/opennl/",
00135     "/trunk/blender/intern/moto/",
00136     )
00137 
00138 ignore_revisions = {2,  # initial commit by Hans
00139                     }
00140 
00141 # important, second value _must_ be the name used by projects.blender.org
00142 # anyone who ever committed to blender
00143 author_name_mapping = {
00144     "alexk": "Alexander Kuznetsov",
00145     "aligorith": "Joshua Leung",
00146     "antont": "Toni Alatalo",
00147     "aphex": "Simon Clitherow",
00148     "artificer": "Ben Batt",
00149     "ascotan": "Joseph Gilbert",
00150     "bdiego": "Diego Borghetti",
00151     "bebraw": "Juho Vepsalainen",
00152     "ben2610": "Benoit Bolsee",
00153     "billrey": "William Reynish",
00154     "bjornmose": "Jens Ole Wund",
00155     "blendix": "Brecht Van Lommel",
00156     "briggs": "Geoffrey Bantle",
00157     "broken": "Matt Ebb",
00158     "campbellbarton": "Campbell Barton",
00159     "cessen": "Nathan Vegdahl",
00160     "cmccad": "Casey Corn",
00161     "damien78": "Damien Plisson",
00162     "desoto": "Chris Burt",
00163     "dfelinto": "Dalai Felinto",
00164     "dingto": "Thomas Dinges",
00165     "djcapelis": "D.J. Capelis",
00166     "dougal2": "Doug Hammond",
00167     "eeshlo": "Alfredo de Greef",
00168     "elubie": "Andrea Weikert",
00169     "ender79": "Andrew Wiggin",  # an alias, not real name.
00170     "erwin": "Erwin Coumans",
00171     "frank": "Frank van Beek",
00172     "genscher": "Daniel Genrich",
00173     "goofster": "Roel Spruit",
00174     "gsrb3d": "gsr b3d",
00175     "guignot": "Jacques Guignot",
00176     "guitargeek": "Johnny Matthews",
00177     "h_xnan": "Hans Lambermont",
00178     "halley": "Ed Halley",
00179     "hans": "Hans Lambermont",
00180     "harkyman": "Roland Hess",
00181     "hos": "Chris Want",
00182     "ianwill": "Willian Padovani Germano",
00183     "imbusy": "Lukas Steiblys",
00184     "intrr": "Alexander Ewering",
00185     "jaguarandi": "Andre Susano Pinto",
00186     "jandro": "Alejandro Conty Estevez",
00187     "jbakker": "Jeroen Bakker",
00188     "jbinto": "Jacques Beuarain",
00189     "jensverwiebe": "Jens Verwiebe",
00190     "jesterking": "Nathan Letwory",
00191     "jhk": "Janne Karhu",
00192     "jiri": "Jiri Hnidek",
00193     "joeedh": "Joseph Eagar",
00194     "jwilkins": "Jason Wilkins",
00195     "kakbarnf": "Robin Allen",
00196     "kazanbas": "Arystanbek Dyussenov",
00197     "kester": "Kester Maddock",
00198     "khughes": "Ken Hughes",
00199     "kwk": "Konrad Kleine",
00200     "larstiq": "Wouter van Heyst",
00201     "letterrip": "Tom Musgrove",
00202     "lmg": "M.G. Kishalmi",
00203     "loczar": "Francis Laurence",  # not 100% sure on this.
00204     "lonetech": "Yann Vernier",
00205     "lukastoenne": "Lukas Toenne",
00206     "lukep": "Jean-Luc Peurière",
00207     "lusque": "Ervin Weber",
00208     "maarten": "Maarten Gribnau",
00209     "mal_cando": "Mal Duffin",
00210     "mein": "Kent Mein",
00211     "merwin": "Mike Erwin",
00212     "mfoxdogg": "Michael Fox",
00213     "mfreixas": "Marc Freixas",
00214     "michel": "Michel Selten",
00215     "migius": "Remigiusz Fiedler",
00216     "mikasaari": "Mika Saari",
00217     "mindrones": "Luca Bonavita",
00218     "mmikkelsen": "Morten Mikkelsen",
00219     "moguri": "Mitchell Stokes",
00220     "mont29": "Bastien Montagne",
00221     "n_t": "Nils Thuerey",
00222     "nazgul": "Sergey Sharybin",
00223     "nexyon": "Joerg Mueller",
00224     "nicholasbishop": "Nicholas Bishop",
00225     "phaethon": "Frederick Lee",
00226     "phase": "Rob Haarsma",
00227     "phlo": "Florian Eggenberger",
00228     "pidhash": "Joilnen Leite",
00229     "psy-fi": "Antony Riakiotakis",
00230     "rwenzlaff": "Robert Wenzlaff",
00231     "sateh": "Stefan Arentz",
00232     "schlaile": "Peter Schlaile",
00233     "scourage": "Robert Holcomb",
00234     "sgefant": "Stefan Gartner",
00235     "sirdude": "sirdude",
00236     "smerch": "Alex Sytnik",
00237     "snailrose": "Charlie Carley",
00238     "stiv": "Stephen Swaney",
00239     "theeth": "Martin Poirier",
00240     "themyers": "Ricki Myers",
00241     "ton": "Ton Roosendaal",
00242     "vekoon": "Elia Sarti",
00243     "xat": "Xavier Thomas",
00244     "xiaoxiangquan": "Xiao Xiangquan",
00245     "zaghaghi": "Hamed Zaghaghi",
00246     "zanqdo": "Daniel Salazar",
00247     "z0r": "Alex Fraser",
00248     "zuster": "Daniel Dunbar",
00249     "jason_hays22": "Jason Hays",
00250     "miikah": "Miika Hamalainen",
00251 
00252     # TODO, find remaining names
00253     "nlin": "",
00254     }
00255 
00256 # lame, fill in empty key/values
00257 empty = []
00258 for key, value in author_name_mapping.items():
00259     if not value:
00260         empty.append(key)
00261 e = None
00262 for e in empty:
00263     author_name_mapping[e] = e.title()
00264 del empty, e
00265 
00266 # useful reverse lookup RealName -> UnixName
00267 author_name_mapping_reverse = {}
00268 for key, value in author_name_mapping.items():
00269     author_name_mapping_reverse[value] = key
00270 
00271 
00272 def build_patch_name_map(filepath):
00273     """ Uses the CSV from the patch tracker to build a
00274         patch <-> author name mapping.
00275     """
00276     patches = {}
00277     import csv
00278     tracker = csv.reader(open(filepath, 'r', encoding='utf-8'), delimiter=';', quotechar='|')
00279     for i, row in enumerate(tracker):
00280         if i == 0:
00281             id_index = row.index("artifact_id")
00282             author_index = row.index("submitter_name")
00283             date_index = row.index("open_date")
00284             status_index = row.index("status_name")  # Open/Closed
00285             min_len = max(id_index, author_index, status_index, date_index)
00286         else:
00287             if len(row) < min_len:
00288                 continue
00289 
00290             # lets just store closed patches, saves time
00291             if row[status_index].strip("\"") == 'Closed':
00292                 patches[int(row[id_index])] = {
00293                     "author": row[author_index].strip("\"").strip().title(),
00294                     "date": row[date_index].strip("\""),
00295                     }
00296     return patches
00297 
00298 
00299 def patch_numbers_from_log(msg):
00300     """ Weak method to pull patch numbers out of a commit log.
00301         rely on the fact that its unlikely any given number
00302         will match up with a closed patch but its possible.
00303     """
00304     patches = []
00305     msg = msg.replace(",", " ")
00306     msg = msg.replace(".", " ")
00307     msg = msg.replace("-", " ")
00308     for w in msg.split():
00309         if      (w[0].isdigit() or
00310                 (len(w) > 2 and w[0] == "[" and w[1] == "#") or
00311                 (len(w) > 1 and w[0] == "#")):
00312 
00313             try:
00314                 num = int(w.strip("[]#"))
00315             except ValueError:
00316                 num = -1
00317 
00318             if num != -1:
00319                 patches.append(num)
00320 
00321     return patches
00322 
00323 
00324 def patch_find_author(commit, patch_map):
00325     patches = patch_numbers_from_log(commit.message)
00326     for p in patches:
00327         if p in patch_map:
00328             patch = patch_map[p]
00329 
00330             '''
00331             print("%r committing patch for %r %d" % (
00332                   author_name_mapping[commit.author],
00333                   patch["author"],
00334                   commit.revision,
00335                   ))
00336             '''
00337 
00338             return p, patch["author"]
00339 
00340     return None, None
00341 
00342 
00343 class Credit(object):
00344     __slots__ = ("commits",
00345                  "is_patch"
00346                  )
00347 
00348     def __init__(self):
00349         self.commits = []
00350         self.is_patch = False
00351 
00352     def contribution_years(self):
00353         years = [int(commit.date.split("-")[0]) for commit in self.commits]
00354         return min(years), max(years)
00355 
00356 
00357 def is_credit_commit_valid(commit):
00358 
00359     if commit.revision in ignore_revisions:
00360         return False
00361 
00362     for msg in ignore_msg:
00363         if msg in commit.message:
00364             return False
00365 
00366     def is_path_valid(path):
00367         if not path.startswith("/trunk/blender/"):
00368             return False
00369         for p in ignore_dir:
00370             if path.startswith(p):
00371                 return False
00372         return True
00373 
00374     tot_valid = 0
00375     for action, path in commit.paths:
00376         if is_path_valid(path):
00377             tot_valid += 1
00378 
00379     if tot_valid == 0:
00380         return False
00381 
00382     # couldnt prove invalid, must be valid
00383     return True
00384 
00385 
00386 def main():
00387     patch_map = build_patch_name_map(tracker_csv)
00388 
00389     commits = parse_commits(svn_log)
00390 
00391     credits = {key: Credit() for key in author_name_mapping}
00392 
00393     for commit in commits:
00394         if is_credit_commit_valid(commit):
00395             patch_id, patch_author = patch_find_author(commit, patch_map)
00396 
00397             if patch_author is None:
00398                 # will error out if we miss adding new devs
00399                 credit = credits[commit.author]
00400             else:
00401                 # so we dont use again
00402                 del patch_map[patch_id]
00403 
00404                 unix_name = author_name_mapping_reverse.get(patch_author)
00405                 if unix_name is None:  # not someone who contributed before
00406                     author_name_mapping_reverse[patch_author] = patch_author
00407                     author_name_mapping[patch_author] = patch_author
00408 
00409                     if patch_author not in credits:
00410                         credits[patch_author] = Credit()
00411 
00412                     credit = credits[patch_author]
00413                     credit.is_patch = True
00414                 else:
00415                     credit = credits[unix_name]
00416 
00417             credit.commits.append(commit)
00418 
00419     # write out the wiki page
00420     # sort by name
00421     file = open("credits.html", 'w', encoding="utf-8")
00422 
00423     file.write("<h3>Individual Contributors</h3>\n\n")
00424 
00425     patch_word = "patch", "patches"
00426     commit_word = "commit", "commits"
00427 
00428     lines = []
00429     for author in sorted(author_name_mapping.keys()):
00430         credit = credits[author]
00431 
00432         if not credit.commits:
00433             continue
00434 
00435         author_real = author_name_mapping[author]
00436         if author_real == author:
00437             name_string = "<b>%s</b>" % author
00438         else:
00439             name_string = "<b>%s</b> (%s)" % (author_real, author)
00440 
00441         credit_range = credit.contribution_years()
00442         if credit_range[0] != credit_range[1]:
00443             credit_range_string = "(%d - %d)" % credit_range
00444         else:
00445             credit_range_string = "- %d" % credit_range[0]
00446 
00447         is_plural = len(credit.commits) > 1
00448 
00449         commit_term = (patch_word[is_plural] if credit.is_patch
00450                        else commit_word[is_plural])
00451 
00452         lines.append("%s, %d %s %s<br />\n" %
00453                      (name_string,
00454                       len(credit.commits),
00455                       commit_term,
00456                       credit_range_string,
00457                       ))
00458     lines.sort()
00459     for line in lines:
00460         file.write(line)
00461     del lines
00462 
00463     file.write("\n\n")
00464 
00465     # -------------------------------------------------------------------------
00466     # Companies, hard coded
00467     file.write("<h3>Contributions from Companies & Organizations</h3>\n")
00468     file.write("<p>\n")
00469     for line in contrib_companies:
00470         file.write("%s<br />\n" % line)
00471     file.write("</p>\n")
00472 
00473     import datetime
00474     now = datetime.datetime.now()
00475     fn = __file__.split("\\")[-1].split("/")[-1]
00476     file.write("<p><center><i>Generated by '%s' %d/%d/%d</i></center></p>\n" %
00477                (fn, now.year, now.month, now.day))
00478 
00479     file.close()
00480 
00481 if __name__ == "__main__":
00482     main()