Blender V2.61 - r43446
|
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()