1#!/usr/bin/env python2.7 2# coding=utf-8 3# 4# Repository Codex Generator by <jaakko.keranen@iki.fi> 5# Generates a set of web pages out of tags in Git commit headlines. 6# License: GNU GPL version 2 (or later) 7 8import os, sys, time, string, base64 9 10OUT_DIR = 'codex' 11TITLE = 'Doomsday Codex' 12 13AUTHOR_ALIASES = { 14 u'Jaakko Keränen': u'skyjake', 15 u'Jaakko Keränen':u'skyjake' 16} 17 18if sys.argv > 1: 19 OUT_DIR = sys.argv[1] 20 21class Commit: 22 def __init__(self, subject, author, date, link, hash): 23 self.subject = subject 24 if author in AUTHOR_ALIASES: 25 self.author = AUTHOR_ALIASES[author] 26 else: 27 self.author = author 28 self.date = date 29 self.link = link 30 self.hash = hash 31 self.extract_tags() 32 33 def add_tag(self, tag): 34 if len(tag) <= 1: return 35 if tag.lower() not in map(lambda x: x.lower(), self.tags): 36 self.tags.append(tag) 37 38 def extract_tags(self): 39 self.tags = [] 40 41 sub = self.subject 42 43 if sub.startswith('Revert "') and sub[-1] == '"': 44 sub = sub[8:-1] 45 self.tags.append(u'Revert') 46 47 self.subject = sub[sub.find(': ') + 1:].strip() 48 49 # Some tags are automatically applied based on the subject line. 50 sensitiveWords = self.subject.replace(';', ' ').replace(':', ' ').replace(',', ' ').split(' ') 51 words = map(lambda w: w.lower(), sensitiveWords) 52 if 'fixed' in words: self.add_tag(u'Fixed') 53 if 'cleanup' in words: self.add_tag(u'Cleanup') 54 if 'debug' in words: self.add_tag(u'Debug') 55 if 'fixed' in words: self.add_tag(u'Fixed') 56 if 'added' in words: self.add_tag(u'Added') 57 if 'refactor' in self.subject.lower(): self.add_tag(u'Refactor') 58 if 'GL' in sensitiveWords: self.add_tag(u'GL') 59 if 'performance' in words: self.add_tag(u'Performance') 60 if 'optimize' in words or 'optimized' in words or 'optimizing' in words: 61 self.add_tag(u'Optimize') 62 63 # Are there any actual tags specified? 64 if sub.find(': ') < 0: return 65 66 sub = sub[:sub.find(': ')].replace(',', '|').replace('/', '|').replace('(', '|').\ 67 replace('&', '|').replace(')', '').replace('Changed ', 'Changed|') 68 69 for unclean in sub.split('|'): 70 unclean = unclean.replace('*nix', 'Unix') 71 tag = unclean.replace('[', '').replace(']', '').replace('*', '').replace('"', '').strip() 72 if 'ccmd' in tag.lower() or 'cvar' in tag.lower(): tag = u'Console' 73 if 'cleanup' in tag.lower() or 'clean up' in tag.lower(): tag = u'Cleanup' 74 if '_' in tag: continue 75 if tag.lower() == 'chex': 76 tag = u'Chex Quest' 77 if tag.lower() == 'hacx': 78 tag = u'HacX' 79 if 'common' in tag.lower(): 80 tag = u'libcommon' 81 if 'doom64' in tag.lower(): 82 tag = u'Doom64' 83 elif 'doom' in tag.lower(): 84 tag = u'Doom' 85 elif 'heretic' in tag.lower(): 86 tag = u'Heretic' 87 elif 'hexen' in tag.lower(): 88 tag = u'Hexen' 89 if tag.lower() == 'add-on repository' or tag.lower() == 'addon repository': 90 tag = u'Add-on Repository' 91 if tag.lower() == 'bsp builder' or tag.lower() == 'bspbuilder': 92 tag = u'BSP Builder' 93 if tag.lower() == 'build repository' or tag.lower() == 'buildrepository': 94 tag = u'Build Repository' 95 if tag.lower() == 'deh reader' or tag.lower() == 'dehreader' or \ 96 tag.lower() == 'deh read' or tag.lower() == 'dehacked reader' or \ 97 tag.lower() == 'dehread': 98 tag = u'Deh Reader' 99 if tag.lower() == 'dpdehread': 100 tag = u'dpDehRead' 101 if tag.lower() == 'gcc': 102 tag = u'GCC' 103 if tag.lower() == 'glsandbox': 104 tag = u'GLSandbox' 105 if tag.lower() == 'dsdirectsound': 106 tag = u'dsDirectSound' 107 if tag.lower() == 'busy mode': 108 tag = u'Busy Mode' 109 if tag.lower() == 'dedicated server': 110 tag = u'Dedicated Server' 111 if tag.lower() == 'cmake': 112 tag = u'CMake' 113 if tag.lower().startswith('filesys') or tag.lower().startswith('file sys'): 114 tag = u'File System' 115 if tag.lower() == 'clang': 116 tag = u'Clang' 117 if 'zone' in tag.lower(): 118 tag = u'Memory Zone' 119 if tag.lower() == 'infine': 120 tag = u'InFine' 121 if 'all games' in tag.lower(): 122 self.add_tag(u'All Games') 123 if tag.lower() == 'lightgrid': 124 tag = u'Light Grid' 125 if tag.lower() == 'render': 126 tag = u'Renderer' 127 if tag.lower() == 'sfx': 128 tag = u'SFX' 129 if tag.lower() == 'taskbarwidth' or tag.lower() == 'taskbar' or tag.lower() == 'taskbarwidget': 130 tag = u'Task Bar' 131 if tag.lower() == 'texture manager': 132 tag = u'Texture Manager' 133 if 'refactoring' in tag.lower() or 'refactored' in tag.lower(): 134 tag = u'Refactor' 135 if 'optimization' in tag.lower() or 'optimize' in tag.lower(): 136 tag = u'Optimize' 137 if tag == 'osx' or tag.lower() == 'mac' or tag.lower() == 'mac os x' or tag.lower() == 'macos': 138 tag = u'OS X' 139 if tag.lower().startswith('fixed'): 140 tag = tag[5:].strip() 141 self.add_tag(u'Fixed') 142 if tag.lower().startswith('added'): 143 tag = tag[5:].strip() 144 self.add_tag(u'Added') 145 if len(tag) > 2 and (tag[0] == '(' and tag[-1] == ')'): 146 tag = tag[1:-1] 147 if tag == u'WadMapConverter': 148 tag = u'Wad Map Converter' 149 if tag.lower() == 'win32': 150 tag = u'Windows' 151 self.add_tag(tag) 152 153def contains_tag(subject): 154 pos = subject.find(': ') 155 if pos < 0: 156 # It might still have some recognized words. 157 for allowed in ['debug', 'optimize', 'cleanup', 'fixed', 'added', 'refactor']: 158 if allowed in subject.lower(): 159 return True 160 if pos > 60: return False 161 for badWord in ['related ', ' up ', ' the ', ' a ', ' in ', ' of', ' on ', 162 ' and ', ' then ', ' when', ' to ', ' for ', ' with ', ' into ']: 163 if badWord in subject[:pos]: 164 return False 165 if subject[pos-4:pos+1] == 'TODO:': return False 166 # It can only contain colons after the tag marker. 167 p = subject.find(':') 168 if p >= 0 and p < pos: return False 169 p = subject.find('.') 170 if p >= 0 and p < pos: return False 171 return True 172 173def fetch_commits(): 174 commits = {} 175 176 tmpName = '__ctmp' 177 format = '[[Subject]]%s[[/Subject]]' + \ 178 '[[Author]]%an[[/Author]]' + \ 179 '[[Date]]%ai[[/Date]]' + \ 180 '[[Link]]http://github.com/skyjake/Doomsday-Engine/commit/%H[[/Link]]' + \ 181 '[[Hash]]%H[[/Hash]]' 182 os.system("git log --format=\"%s\" >> %s" % (format, tmpName)) 183 logText = unicode(file(tmpName, 'rt').read(), 'utf-8') 184 os.remove(tmpName) 185 186 pos = 0 187 while True: 188 pos = logText.find('[[Subject]]', pos) 189 if pos < 0: break # No more. 190 end = logText.find('[[/Subject]]', pos) 191 subject = logText[pos+11:end] 192 193 # Author. 194 pos = logText.find('[[Author]]', pos) 195 end = logText.find('[[/Author]]', pos) 196 author = logText[pos+10:end] 197 198 # Date. 199 pos = logText.find('[[Date]]', pos) 200 end = logText.find('[[/Date]]', pos) 201 date = logText[pos+8:end] 202 203 # Link. 204 pos = logText.find('[[Link]]', pos) 205 end = logText.find('[[/Link]]', pos) 206 link = logText[pos+8:end] 207 208 # Hash. 209 pos = logText.find('[[Hash]]', pos) 210 end = logText.find('[[/Hash]]', pos) 211 hash = logText[pos+8:end] 212 213 if contains_tag(subject): 214 commits[hash] = Commit(subject, author, date, link, hash) 215 216 return commits 217 218# Compile the commit database. 219byHash = fetch_commits() 220byTag = {} 221for commit in byHash.values(): 222 # Index all commits by tag. 223 for tag in commit.tags: 224 if tag in byTag: 225 if commit not in byTag[tag]: 226 byTag[tag].append(commit) 227 else: 228 byTag[tag] = [commit] 229 230relatedTags = [ 231 ['Fixed', 'Added', 'Refactor', 'Optimize', 'Revert', 'Cleanup', 'Debug'], 232 ['Windows', 'OS X', 'macOS', 'Linux', 'Unix', 'Debian', 'Ubuntu', 'FreeBSD', 'X11', '64-bit'], 233 ['Windows', 'Windows *'], 234 ['Qt', 'SDL'], 235 ['Builder', 'Builds', 'qmake', 'Project*', 'CMake', 'Distrib', 'GCC', 'MSVC', 'Clang', 'Git', 236 'TextMate', 'wikidocs'], 237 ['Test*', 'GLSandbox'], 238 ['Docs', 'Documentation', 'Codex', 'Readme', 'Doxygen', 'Amethyst'], 239 ['Homepage', 'Add-on Repository', 'Build Repository', 'CSS', 'RSS', 'DEW', 'Forums'], 240 ['libdeng2', 'libcore', 'App', 'Config', 'Log', 'LogBuffer', 'Widgets', 'Garbage', 'Garbages'], 241 ['libgui', 'DisplayMode', 'GL', 'OpenGL', 'GL*', 'Atlas*', 'Image', '*Bank', 'Font'], 242 ['libdeng', 'libdeng1', 'liblegacy', 'Str'], 243 ['libshell', 'Shell', 'AbstractLineEditor', 'Link'], 244 ['Client', 'Client UI', 'UI', 'Console', 'Control Panel', 'Default Style', 'GameSelectionWidget', 245 'Task Bar', 'Updater', 'WindowSystem', 'Client*'], 246 ['Server', 'Dedicated Server', 'Dedicated Mode', 'Server*'], 247 ['Snowberry', 'Shell', 'Amethyst', 'md2tool'], 248 ['All Games', 'Doom', 'Heretic', 'Hexen', 'Doom64', 'Chex Quest', 'HacX'], 249 ['Plugins', 'Wad Map Converter', 'Deh Reader', 'dsDirectSound', 'OpenAL', 'dpDehRead', 250 'dsWinMM', 'Dummy Audio', 'Example Plugin', 'exampleplugin', 'FluidSynth', 'FMOD'], 251 ['API', 'DMU', 'DMU API'], 252 ['DED', 'DED Parser', 'Ded Reader', 'Definitions', 'Info', 'ScriptedInfo'], 253 ['Ring Zero', 'GameSelectionWidget'], 254 ['Script*', 'scriptsys', 'Script', 'Record', 'Variable'], 255 ['File System', 'Folder', 'Feed', 'FS', 'FS1', 'File*', '*File'], 256 ['Resource*', 'Material*', 'Texture*', 'Uri'], 257 ['Renderer', '* Renderer', 'Model*', 'Light Grid'], 258 ['Network', 'Multiplayer', 'Server', 'Protocol'], 259 ['Concurrency', 'Task', 'libdeng2'], 260 ['libcommon', 'Game logic', 'Menu', 'Game Menu', 'Game Save', 'Games', 'Automap'], 261 ['World', 'GameMap', 'Map', 'BSP Builder', 'HEdge', 'Bsp*', 'Line*', 'Sector', 'SectionEdge', 'Wall*', 262 'Blockmap', 'Polyobj', 'Plane'], 263 ['Finale Interpreter', 'Finales', 'InFine'], 264 ['Input*', 'Bindings', 'Joystick', 'MouseEvent', 'KeyEvent', 'libgui'], 265 ['Audio*', 'SFX', 'Music', 'FluidSynth'], 266 ['Widget*', '*Widget', '*Rule', 'Rule*', 'Animation' ] 267] 268 269def find_related_tags(tag): 270 rels = [] 271 for group in relatedTags: 272 # Any wildcards? 273 tags = [] 274 for t in group: 275 if '*' not in t: 276 tags.append(t) 277 elif t[0] == '*': 278 for x in byTag.keys(): 279 if x.endswith(t[1:]): 280 tags.append(x) 281 elif t[-1] == '*': 282 for x in byTag.keys(): 283 if x.startswith(t[:-1]): 284 tags.append(x) 285 if tag not in tags: continue 286 for t in tags: 287 if t != tag and t not in rels: 288 rels.append(t) 289 return sorted(rels, key=lambda s: s.lower()) 290 291if not os.path.exists(OUT_DIR): os.mkdir(OUT_DIR) 292 293def tag_filename(tag): 294 return base64.urlsafe_b64encode(tag) 295 296def encoded_text(logText): 297 logText = logText.replace(u'&', u'&') 298 logText = logText.replace(u'ä', u'ä') 299 logText = logText.replace(u'ö', u'ö') 300 logText = logText.replace(u'Ä', u'Ä') 301 logText = logText.replace(u'Ö', u'Ö') 302 logText = logText.replace(u'<', u'<') 303 logText = logText.replace(u'>', u'>') 304 logText = filter(lambda c: c in string.whitespace or c > ' ', logText) 305 return logText 306 307def print_header(out, pageTitle): 308 print >> out, '<!DOCTYPE html>' 309 print >> out, '<html>' 310 print >> out, '<head>' 311 print >> out, '<meta charset="UTF-8">' 312 print >> out, '<title>%s - %s</title>' % (pageTitle, TITLE) 313 print >> out, '<style type="text/css">' 314 print >> out, 'body, table { font-family: "Helvetica Neue", sans-serif; }' 315 print >> out, 'a { color: black; text-decoration: none; }' 316 print >> out, 'a:hover { text-decoration: underline; }' 317 print >> out, '.skiplist { line-height: 150%; }' 318 print >> out, 'table { width: 100%; }' 319 print >> out, 'table td { vertical-align: top; }' 320 print >> out, 'td.commit-date { width: 100px; padding-left: 1.5ex; }' 321 print >> out, 'td.commit-tags { width: 200px; text-align: right; }' 322 print >> out, '.footer { text-align: center; font-size: 80%; }' 323 print >> out, '.end-symbol { font-size: 125%; }' 324 print >> out, '</style>' 325 print >> out, '</head>' 326 print >> out, '<body>' 327 print >> out, '<form id="findform" name="find" action="find_tag.php" method="get"><a href="index.html">Alphabetical Index</a> | <a href="index_by_size.html">Tags by Size</a> | Find tag: <input type="text" name="tag"> <input type="submit" value="Find"></form>' 328 print >> out, '<h1>%s</h1>' % pageTitle 329 330def print_footer(out): 331 print >> out, '<div class="footer"><p class="end-symbol">⊕</p><p>This is the <a href="http://dengine.net/">Doomsday Engine</a> <a href="http://github.com/skyjake/Doomsday-Engine/">source code repository</a> commit tag index.</p><p>Last updated: %s</p></div>' % time.asctime() 332 print >> out, '</body>' 333 print >> out, '</html>' 334 335def print_table(out, cells, cell_printer, numCols=4, tdStyle='', separateByFirstLetter=False, letterFunc=None): 336 colSize = (len(cells) + numCols - 1) / numCols 337 tdElem = '<td style="width: %i%%; %s">' % (100/numCols, tdStyle) 338 print >> out, '<table><tr>' + tdElem 339 340 idx = 0 341 inCol = 0 342 letter = '' 343 344 while idx < len(cells): 345 if separateByFirstLetter: 346 if inCol > 0 and letter != '' and letter != letterFunc(cells[idx]): 347 print >> out, '<br/>' 348 letter = letterFunc(cells[idx]) 349 350 cell_printer(out, cells[idx]) 351 352 idx += 1 353 inCol += 1 354 if inCol == colSize: 355 print >> out, tdElem 356 inCol = 0 357 358 print >> out, '</table>' 359 360# Create the index page with all tags sorted alphabetically. 361out = file(os.path.join(OUT_DIR, 'index.html'), 'wt') 362print_header(out, 'Alphabetical Tag Index') 363 364sortedTags = sorted(byTag.keys(), cmp=lambda a, b: cmp(a.lower(), b.lower())) 365 366def alpha_index_cell(out, tag): 367 count = len(byTag[tag]) 368 if count > 100: 369 style = 'font-weight: bold' 370 elif count < 5: 371 style = 'color: #aaa' 372 else: 373 style = '' 374 print >> out, '<a href="tag_%s.html"><span style="%s">%s (%i)</span></a><br/>' % ( 375 tag_filename(tag), style, encoded_text(tag), count) 376 377print_table(out, sortedTags, alpha_index_cell, separateByFirstLetter=True, 378 letterFunc=lambda t: t[0].lower()) 379 380print_footer(out) 381 382# Create the index page with tags sorted by size. 383out = file(os.path.join(OUT_DIR, 'index_by_size.html'), 'wt') 384print_header(out, 'All Tags by Size') 385 386def tag_size_comp(a, b): 387 c = cmp(len(byTag[b]), len(byTag[a])) 388 if c == 0: return cmp(a.lower(), b.lower()) 389 return c 390 391def size_index_cell(out, tag): 392 count = len(byTag[tag]) 393 if count > 100: 394 style = 'font-weight: bold' 395 elif count < 5: 396 style = 'color: #aaa' 397 else: 398 style = '' 399 print >> out, '<a href="tag_group_%s.html"><span style="%s">%i %s</span></a><br/>' % ( 400 tag_filename(tag), style, count, encoded_text(tag)) 401 402print_table(out, sorted(byTag.keys(), cmp=tag_size_comp), size_index_cell) 403 404print_footer(out) 405 406def print_tags(out, com, tag, linkSuffix=''): 407 first = True 408 for other in sorted(com.tags, cmp=lambda a, b: cmp(a.lower(), b.lower())): 409 if other != tag: 410 if not first: print >> out, '| ' 411 first = False 412 print >> out, '<b><a href="tag_%s%s.html">%s</a></b>' % (linkSuffix, \ 413 tag_filename(other), encoded_text(other)) 414 415def print_commit(out, com, tag, linkSuffix=''): 416 print >> out, trElem + '<td class="commit-date"><a href="%s">' % com.link + com.date[:10] + '</a> ' 417 print >> out, '<td class="commit-tags">' 418 print_tags(out, com, tag, linkSuffix) 419 print >> out, '<td>:' 420 print >> out, ('<td><a href="%s">' % com.link + encoded_text(com.subject)[:250] + '</a>').encode('utf8') 421 422colors = ['#f00', '#0a0', '#00f', 423 '#ed0', '#f80', '#0ce', 424 '#88f', '#b0b', 425 '#f99', '#8d8', '#222', 426 '#44a', '#940', '#888'] 427 428def html_color(colIdx): 429 if colIdx == None: return '#000' 430 return colors[colIdx % len(colors)] 431 432def color_box(colIdx): 433 return ' <span style="padding-left:1em; background-color:%s"> </span> ' % \ 434 html_color(colIdx) 435 436def print_date_sorted_commits(out, coms, tag, linkSuffix='', colorIdx=None): 437 months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 438 'September', 'October', 'November', 'December'] 439 curDate = '' 440 dateSorted = sorted(coms, key=lambda c: c.date, reverse=True) 441 print >> out, '<table>' 442 for com in dateSorted: 443 # Monthly headers. 444 if curDate == '' or curDate != com.date[:7]: 445 print >> out, '<tr><td colspan="4" style="padding-left:1ex; padding-top:1.5em; padding-bottom:0.5em; font-weight:bold; font-size:110%%; color:%s; border-bottom:1px dotted %s">' % \ 446 (html_color(colorIdx), html_color(colorIdx)) 447 print >> out, months[int(com.date[5:7]) - 1], com.date[:4] 448 curDate = com.date[:7] 449 print_commit(out, com, tag, linkSuffix) 450 print >> out, '</table>' 451 452# Create the tag redirecting PHP page. 453out = file(os.path.join(OUT_DIR, 'find_tag.php'), 'wt') 454print >> out, '<?php' 455 456print >> out, '$tags = array(', string.join(['"%s" => "%s"' % (tag, tag_filename(tag)) for tag in byTag.keys()], ', '), ');' 457print >> out, """ 458$input = stripslashes(strip_tags($_GET["tag"])); 459$style = $_GET["style"]; 460if($style == 'grouped') { 461 $style = 'group_'; 462} else { 463 $style = ''; 464} 465$destination = "index.html"; 466$best = -1.0; 467if(strlen($input) > 0 && strlen($input) < 60) { 468 foreach($tags as $tag => $link) { 469 $lev = (float) levenshtein($input, $tag); // case sensitive 470 if(stripos($tag, $input) !== FALSE || stripos($input, $tag) !== FALSE) { 471 // Found as a case insensitive substring, increase likelihood. 472 $lev = $lev/2.0; 473 } 474 if(stripos($tag, $input) === 0) { 475 // Increase likelihood further if the match is in the beginning. 476 $lev = $lev/2.0; 477 } 478 if(!strcasecmp($tag, $input) == 0) { 479 // Case insensitive direct match, increase likelihood. 480 $lev = $lev/2.0; 481 } 482 if($lev == 0) { 483 $destination = "tag_$style$link.html"; 484 break; 485 } 486 if($best < 0 || $lev < $best) { 487 $destination = "tag_$style$link.html"; 488 $best = $lev; 489 } 490 } 491} 492header("Location: $destination"); 493""" 494 495print >> out, "?>" 496 497def print_related_tags(out, tag, style=''): 498 rels = find_related_tags(tag) 499 if len(rels) == 0: return 500 print >> out, '<p>Related tags: ' 501 print >> out, string.join(['<a href="tag_%s%s.html">%s</a>' % (style, tag_filename(t), t) 502 for t in rels], ', ') 503 print >> out, '</p>' 504 505def percentage(part, total): 506 return int(round(float(part)/float(total) * 100)) 507 508def print_authorship(out, tag): 509 authors = {} 510 total = len(byTag[tag]) 511 for commit in byTag[tag]: 512 if commit.author in authors: 513 authors[commit.author] += 1 514 else: 515 authors[commit.author] = 1 516 sortedAuthors = sorted(authors.keys(), key=lambda a: authors[a], reverse=True) 517 print >> out, '<p>Authorship:', string.join(['%i%% %s' % (percentage(authors[a], total), a) 518 for a in sortedAuthors], ', ').encode('utf8'), '</p>' 519 520# 521# Create pages for each tag. 522# 523for tag in byTag.keys(): 524 #print 'Generating tag page:', tag 525 526 trElem = '<tr style="padding:1ex">' 527 528 # First a simple date-based list of commits in this tag. 529 out = file(os.path.join(OUT_DIR, 'tag_%s.html' % tag_filename(tag)), 'wt') 530 print_header(out, tag) 531 print_related_tags(out, tag) 532 print_authorship(out, tag) 533 print >> out, '<p><a href="tag_group_%s.html"><b>View commits by groups</b></a></p>' % tag_filename(tag) 534 print >> out, '<div style="margin-left:1em">' 535 print_date_sorted_commits(out, byTag[tag], tag) 536 print >> out, '</div>' 537 print_footer(out) 538 539 present = {} 540 for com in byTag[tag]: 541 for other in com.tags: 542 if other == tag: continue 543 if other not in present: 544 present[other] = [com] 545 else: 546 present[other].append(com) 547 548 def present_by_size(a, b): 549 c = cmp(len(present[b]), len(present[a])) 550 if c == 0: return cmp(a.lower(), b.lower()) 551 return c 552 553 presentSorted = sorted(present.keys(), cmp=present_by_size) 554 555 # Then grouped by subgroup size. 556 out = file(os.path.join(OUT_DIR, 'tag_group_%s.html' % tag_filename(tag)), 'wt') 557 print_header(out, tag + ' (Grouped)') 558 print_related_tags(out, tag, 'group_') 559 print_authorship(out, tag) 560 print >> out, '<p><a id="top"></a><a href="tag_%s.html"><b>View commits by date</b></a></p>' % tag_filename(tag) 561 562 if len(byTag[tag]) > 10: 563 print >> out, '<div class="skiplist"><b>Jump down to:</b>' 564 565 class SkipTagPrinter: 566 def __init__(self): 567 self.color = 0 568 569 def __call__(self, out, tag): 570 print >> out, '<span style="white-space:nowrap;">' + color_box(self.color) 571 self.color += 1 572 print >> out, '<a href="#%s">%s (%i)</a></span><br/>' % (tag_filename(tag), 573 tag, len(present[tag])) 574 575 print_table(out, presentSorted, SkipTagPrinter(), numCols=5, tdStyle='line-height:150%') 576 577 print >> out, '</div>' 578 579 # First the commits without any subgroups. 580 print >> out, '<div style="margin-left:1em">' 581 print_date_sorted_commits(out, filter(lambda c: len(c.tags) == 1, byTag[tag]), tag) 582 print >> out, '</div>' 583 584 color = 0 585 for tag2 in presentSorted: 586 print >> out, '<h2><a id="%s"></a>%s<a href="tag_group_%s.html">%s</a> (%i) <span style="color:#ccc">— %s</span></h2>' % ( 587 tag_filename(tag2), color_box(color), tag_filename(tag2), tag2, len(present[tag2]), tag) 588 print >> out, '<div style="margin-left:0.85em; border-left:4px solid %s">' % html_color(color) 589 print_date_sorted_commits(out, filter(lambda c: tag2 in c.tags, byTag[tag]), tag, 'group_', 590 colorIdx=color) 591 print >> out, '</div>' 592 print >> out, '<p><small><a href="#top">↑ Back to top</a></small></p>' 593 color += 1 594 595 print_footer(out) 596