#! /usr/bin/env python3 # Produces a table of filenames x commits, showing where a file was touched. # Useful for rebasing feature branch into reasonable commits. # usage: # git log --name-status --oneline origin/master.. | $0 # git log --name-status --oneline origin/master.. | $0 --html > file.html # A variant can also be found at https://secure.phabricator.com/P1998 import argparse import sys args_parser = None def parse_args(): global args_parser usage = \ '''git log --name-status --oneline origin/master.. | %(prog)s --html > file.html git log --name-status --oneline origin/master.. | %(prog)s ''' args_parser = argparse.ArgumentParser( description='Show nice table to assist cleaning up git commits.', usage=usage, ) args_parser.add_argument('-H', '--html', dest='mode', action='store_const', const='html', default='text', help='Output formatted HTML table') args_parser.add_argument('-t', '-T', '--text', dest='mode', action='store_const', const='text', default='text', help='Output plain-text table') return args_parser.parse_args() def collect_data(): files = set() commits = list() class Commit(object): def __init__(self, title): self.title = title self.files = dict() def addFile(self, filename, status): self.files[filename] = status curr_commit = None for line in sys.stdin: try: if line[0] in '0123456789abcdef': curr_commit = Commit(line) commits.insert(0, curr_commit) elif line[0] == 'R': v, f1, f2 = line.split('\t') f1 = f1.strip() f2 = f2.strip() curr_commit.addFile(f1, '\u2563') curr_commit.addFile(f2, '\u2560') files.add(f1) files.add(f2) else: v, f = line.split('\t', 2) f = f.strip() curr_commit.addFile(f, v) files.add(f) except: print(line) raise return commits, files def output_html(commits, files): if sys.stdout.isatty(): print("Output is set to html, redirect this to file.") sys.exit(4) CSS = ''' td { border: 1px solid black; } table { border-collapse: collapse; } td:nth-child(even), th.rotate:nth-child(even) > div > span { background: #CCC } th.rotate { height: 350px; } th.rotate > div { transform: translate(20px, 164px) rotate(315deg); transform-origin: left; width: 30px; white-space: nowrap; } th.rotate > div > span { border-bottom: 1px solid black; padding: 5px 0; } tbody th { text-align: right; } tbody td { text-align: center; } ''' print('') print(''' ''') print('') print(len(commits) * '') print(''' ''') for c in commits: print('') print('') for f in sorted(files): print('') print('' % f, end='') for c in commits: s = c.files.get(f, ' ') print('' % s, end='') print('') print('
') print(c.title) print('
%s%s
') def output_text(commits, files): name_len = 0 for f in files: name_len = max(name_len, len(f)) name_pattern = '%' + str(name_len) + 's' for f in sorted(files): print(name_pattern % f, end=' ') for c in commits: s = c.files.get(f, ' ') print(s, end=' ') print() args = parse_args() if sys.stdin.isatty(): print("\n\t\tstdin is tty, not processing.\n") args_parser.print_help() sys.exit(1) commits, files = collect_data() if args.mode == 'text': output_text(commits, files) elif args.mode == 'html': output_html(commits, files) else: print("500 Internal Server Error") args_parser.print_help() sys.exit(3)