#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2023 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later """ This is a tool for reviewing commit ranges, writing into accept/reject files. Useful for reviewing revisions to backport to stable builds. Example usage: ./git_log_review_commits.py --source=../../.. --range=HEAD~40..HEAD --filter=BUGFIX """ class _Getch: """ Gets a single character from standard input. Does not echo to the screen. """ def __init__(self): try: self.impl = _GetchWindows() except ImportError: self.impl = _GetchUnix() def __call__(self): return self.impl() class _GetchUnix: def __init__(self): import tty import sys def __call__(self): import sys import tty import termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class _GetchWindows: def __init__(self): import msvcrt def __call__(self): import msvcrt return msvcrt.getch() getch = _Getch() # ------------------------------------------------------------------------------ # Pretty Printing USE_COLOR = True if USE_COLOR: color_codes = { 'black': '\033[0;30m', 'bright_gray': '\033[0;37m', 'blue': '\033[0;34m', 'white': '\033[1;37m', 'green': '\033[0;32m', 'bright_blue': '\033[1;34m', 'cyan': '\033[0;36m', 'bright_green': '\033[1;32m', 'red': '\033[0;31m', 'bright_cyan': '\033[1;36m', 'purple': '\033[0;35m', 'bright_red': '\033[1;31m', 'yellow': '\033[0;33m', 'bright_purple': '\033[1;35m', 'dark_gray': '\033[1;30m', 'bright_yellow': '\033[1;33m', 'normal': '\033[0m', } def colorize(msg, color=None): return (color_codes[color] + msg + color_codes['normal']) else: def colorize(msg, color=None): return msg bugfix = "" # avoid encoding issues import os import sys import io sys.stdin = os.fdopen(sys.stdin.fileno(), "rb") sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True) sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True) def print_commit(c): print("------------------------------------------------------------------------------") print(colorize("{{GitCommit|%s}}" % c.sha1.decode(), color='green'), end=" ") # print("Author: %s" % colorize(c.author, color='bright_blue')) print(colorize(c.author, color='bright_blue')) print() print(colorize(c.body, color='normal')) print() print(colorize("Files: (%d)" % len(c.files_status), color='yellow')) for f in c.files_status: print(colorize(" %s %s" % (f[0].decode('ascii'), f[1].decode('ascii')), 'yellow')) print() def argparse_create(): import argparse # When --help or no args are given, print this help usage_text = "Review revisions." epilog = "This script is typically used to help write release notes" 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( "--author", dest="author", metavar='AUTHOR', type=str, required=False, help=("Method to filter commits in ['BUGFIX', todo]")) parser.add_argument( "--filter", dest="filter_type", metavar='FILTER', type=str, required=False, help=("Method to filter commits in ['BUGFIX', todo]")) return parser def main(): ACCEPT_FILE = "review_accept.txt" REJECT_FILE = "review_reject.txt" # ---------- # Parse Args args = argparse_create().parse_args() from git_log import GitCommitIter # -------------- # Filter Commits def match(c): # filter_type if not args.filter_type: pass elif args.filter_type == 'BUGFIX': first_line = c.body.strip().split("\n")[0] assert len(first_line) if any(w for w in first_line.split() if w.lower().startswith(("fix", "bugfix", "bug-fix"))): pass else: return False elif args.filter_type == 'NOISE': first_line = c.body.strip().split("\n")[0] assert len(first_line) if any(w for w in first_line.split() if w.lower().startswith("cleanup")): pass else: return False else: raise Exception("Filter type %r isn't known" % args.filter_type) # author if not args.author: pass elif args.author != c.author: return False return True commits = [c for c in GitCommitIter(args.source_dir, args.range_sha1) if match(c)] # oldest first commits.reverse() tot_accept = 0 tot_reject = 0 def exit_message(): print(" Written", colorize(ACCEPT_FILE, color='green'), "(%d)" % tot_accept, colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject, ) for i, c in enumerate(commits): if os.name == "posix": # Also clears scroll-back. os.system("tput reset") else: print('\x1b[2J') # clear sha1 = c.sha1 # diff may scroll off the screen, that's OK os.system("git --git-dir %s show %s --format=%%n" % (c._git_dir, sha1.decode('ascii'))) print("") print_commit(c) sys.stdout.flush() # print(ch) while True: print("Space=" + colorize("Accept", 'green'), "Enter=" + colorize("Skip", 'red'), "Ctrl+C or Q=" + colorize("Quit", color='white'), "[%d of %d]" % (i + 1, len(commits)), "(+%d | -%d)" % (tot_accept, tot_reject), ) ch = getch() if ch == b'\x03' or ch == b'q': # Ctrl+C exit_message() print("Goodbye! (%s)" % c.sha1.decode()) return elif ch == b' ': log_filepath = ACCEPT_FILE tot_accept += 1 break elif ch == b'\r': log_filepath = REJECT_FILE tot_reject += 1 break else: print("Unknown input %r" % ch) with open(log_filepath, 'ab') as f: f.write(sha1 + b'\n') exit_message() if __name__ == "__main__": main()