tornavis/tools/utils/git_log_review_commits_adva...

773 lines
28 KiB
Python
Executable File

#!/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,
and optionally generate release-log-ready data.
Useful for reviewing revisions to back-port to stable builds.
Note that, if any of the data files generated already exist, they will be extended
with new revisions, not overwritten.
Note that, for the most complex 'wiki-ready' file generated by `--accept-releaselog`,
proof-reading after this tool has ran is heavily suggested!
Example usage:
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79
To add list of fixes between RC2 and RC3, and list both RC2 and RC3 fixes also in their own sections:
./git_log_review_commits_advanced.py --source ../../.. --range <RC2 revision>..<RC3 revision> --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79 --blender-rstate=RC3 --blender-rstate-list="RC2,RC3"
To exclude all commits from some given files, by sha1 or by commit message (from previously generated release logs) - much handy when going over commits which were partially cherry-picked into a previous release branch e.g.:
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --filter-exclude-sha1-fromfiles "review_accept.txt" "review_reject.txt" --filter-exclude-fromreleaselogs "review_accept_release_log.txt" --accept-pretty --accept-releaselog --blender-rev 2.75
"""
import os
import sys
import io
import re
ACCEPT_FILE = "review_accept.txt"
REJECT_FILE = "review_reject.txt"
ACCEPT_LOG_FILE = "review_accept_log.txt"
ACCEPT_PRETTY_FILE = "review_accept_pretty.txt"
ACCEPT_RELEASELOG_FILE = "review_accept_release_log.txt"
IGNORE_START_LINE = "<!-- IGNORE_START -->"
IGNORE_END_LINE = "<!-- IGNORE_END -->"
_cwd = os.getcwd()
__doc__ = __doc__ + \
"\nRaw GIT revisions files:\n\t* Accepted: %s\n\t* Rejected: %s\n\n" \
"Basic log accepted revisions: %s\n\nWiki-printed accepted revisions: %s\n\n" \
"Full release notes wiki page: %s\n" \
% (os.path.join(_cwd, ACCEPT_FILE), os.path.join(_cwd, REJECT_FILE),
os.path.join(_cwd, ACCEPT_LOG_FILE), os.path.join(_cwd, ACCEPT_PRETTY_FILE),
os.path.join(_cwd, ACCEPT_RELEASELOG_FILE))
del _cwd
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 = ""
BUGFIX_CATEGORIES = (
("Objects / Animation / GP", (
"Animation",
"Constraints",
"Grease Pencil",
"Objects",
"Dependency Graph",
),
),
("Data / Geometry", (
"Armatures",
"Curve/Text Editing",
"Mesh Editing",
"Meta Editing",
"Modifiers",
"Material / Texture",
),
),
("Physics / Simulations / Sculpt / Paint", (
"Particles",
"Physics / Hair / Simulations",
"Sculpting / Painting",
),
),
("Image / Video / Render", (
"Image / UV Editing",
"Masking",
"Motion Tracking",
"Movie Clip Editor",
"Nodes / Compositor",
"Render",
"Render: Cycles",
"Render: Freestyle",
"Sequencer",
),
),
("UI / Spaces / Transform", (
"3D View",
"Input (NDOF / 3D Mouse)",
"Outliner",
"Text Editor",
"Transform",
"User Interface",
),
),
("Game Engine", (
),
),
("System / Misc", (
"Audio",
"Collada",
"File I/O",
"Other",
"Python",
"System",
),
),
)
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 gen_commit_summary(c):
# In git, all commit message lines until first empty one are part of 'summary'.
return c.body.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
def print_commit(c):
print("------------------------------------------------------------------------------")
print(colorize(c.sha1.decode(), color='green'), end=" ")
print(colorize(c.date.strftime("%Y/%m/%d"), color='purple'), end=" ")
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 gen_commit_log(c):
return "rB%s %s %-30s %s" % (c.sha1.decode()[:10], c.date.strftime("%Y/%m/%d"),
c.author, gen_commit_summary(c))
re_bugify_str = r"T([0-9]{1,})"
re_bugify = re.compile(re_bugify_str)
re_commitify = re.compile(r"\W(r(?:B|BA|BAC|BTS)[0-9a-fA-F]{6,})")
re_prettify = re.compile(r"(.{,20}?)(Fix(?:ing|es)?\s*(?:for)?\s*" + re_bugify_str + r")\s*[-:,]*\s*", re.IGNORECASE)
def gen_commit_message_pretty(c, unreported=None):
body = gen_commit_summary(c)
tbody = re_prettify.sub(r"Fix {{BugReport|\3}}: \1", body)
if tbody == body:
if unreported is not None:
unreported[0] = True
tbody = "Fix unreported: %s" % body
body = re_bugify.sub(r"{{BugReport|\1}}", tbody)
body = re_commitify.sub(r"{{GitCommit|\1}}", body)
return body
def gen_commit_pretty(c, unreported=None, rstate=None):
body = gen_commit_message_pretty(c, unreported)
if rstate is not None:
return "* [%s] %s ({{GitCommit|rB%s}})." % (rstate, body, c.sha1.decode()[:10])
return "* %s ({{GitCommit|rB%s}})." % (rstate, body, c.sha1.decode()[:10])
def gen_commit_unprettify(body):
if body.startswith("* ["):
end = body.find("]")
if end > 0:
body = body[end + 2:] # +2 to remove ] itself, and following space.
start = body.rfind("({{GitCommit|rB")
if start > 0:
body = body[:start - 1] # -1 to remove trailing space.
return body
def print_categories_tree():
for i, (main_cat, sub_cats) in enumerate(BUGFIX_CATEGORIES):
print("\t[%d] %s" % (i, main_cat))
for j, sub_cat in enumerate(sub_cats):
print("\t\t[%d] %s" % (j, sub_cat))
def release_log_extract_messages(path):
messages = set()
if os.path.exists(path):
with open(path, 'r') as f:
ignore = False
header = True
for l in f:
if IGNORE_END_LINE in l:
ignore = False
continue
elif ignore or IGNORE_START_LINE in l:
ignore = True
continue
l = l.strip(" \n")
if header and not l.startswith("=="):
continue # Header, we don't care here.
header = False
if not l.startswith("==") and "Fix " in l:
messages.add(gen_commit_unprettify(l))
return messages
def release_log_init(path, source_dir, blender_rev, start_sha1, end_sha1, rstate, rstate_list):
from git_log import GitRepo
if rstate is not None:
header = "= Blender %s: Bug Fixes =\n\n" \
"[%s] Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
% (blender_rev, rstate, start_sha1[:10], end_sha1[:10])
else:
header = "= Blender %s: Bug Fixes =\n\n" \
"Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
% (blender_rev, start_sha1[:10], end_sha1[:10])
release_log = {"__HEADER__": header, "__COUNT__": [0, 0], "__RSTATES__": {k: [] for k in rstate_list}}
if os.path.exists(path):
branch = GitRepo(source_dir).branch.decode().strip()
sub_cats_to_main_cats = {s_cat: m_cat[0] for m_cat in BUGFIX_CATEGORIES for s_cat in m_cat[1]}
main_cats = {m_cat[0] for m_cat in BUGFIX_CATEGORIES}
with open(path, 'r') as f:
header = []
main_cat = None
sub_cat = None
ignore = False
for l in f:
if IGNORE_END_LINE in l:
ignore = False
continue
elif ignore or IGNORE_START_LINE in l:
ignore = True
continue
l = l.strip(" \n")
if not header:
header.append(l)
for hl in f:
if IGNORE_END_LINE in hl:
ignore = False
continue
elif ignore or IGNORE_START_LINE in hl:
ignore = True
continue
hl = hl.strip(" \n")
if hl.startswith("=="):
main_cat = hl.strip(" =")
if main_cat not in main_cats:
sub_cat = main_cat
main_cat = sub_cats_to_main_cats.get(main_cat, None)
else:
sub_cat = None
# print("hl MAINCAT:", hl, main_cat, " | ", sub_cat)
break
header.append(hl)
if rstate is not None:
release_log["__HEADER__"] = "%s[%s] Changes from revision {{GitCommit|%s}} to " \
"{{GitCommit|%s}}, inclusive (''%s'' branch).\n\n" \
"" % ("\n".join(header), rstate,
start_sha1[:10], end_sha1[:10], branch)
else:
release_log["__HEADER__"] = "%sChanges from revision {{GitCommit|%s}} to {{GitCommit|%s}}, " \
"inclusive (''%s'' branch).\n\n" \
"" % ("\n".join(header), start_sha1[:10], end_sha1[:10], branch)
count = release_log["__COUNT__"] = [0, 0]
continue
if l.startswith("==="):
sub_cat = l.strip(" =")
if sub_cat in sub_cats_to_main_cats:
main_cat = sub_cats_to_main_cats.get(sub_cat, None)
elif sub_cat in main_cats:
main_cat = sub_cat
sub_cat = None
else:
main_cat = None
# print("l SUBCAT:", l, main_cat, " | ", sub_cat)
elif l.startswith("=="):
main_cat = l.strip(" =")
if main_cat not in main_cats:
sub_cat = main_cat
main_cat = sub_cats_to_main_cats.get(main_cat, None)
else:
sub_cat = None
# print("l MAINCAT:", l, main_cat, " | ", sub_cat)
elif "Fix " in l:
if "Fix {{BugReport|" in l:
main_cat_data, _ = release_log.setdefault(main_cat, ({}, {}))
main_cat_data.setdefault(sub_cat, []).append(l)
count[0] += 1
# print("l REPORTED:", l)
else:
_, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
main_cat_data_unreported.setdefault(sub_cat, []).append(l)
count[1] += 1
# print("l UNREPORTED:", l)
l_rstate = l.strip("* ")
if l_rstate.startswith("["):
end = l_rstate.find("]")
if end > 0:
rstate = l_rstate[1:end]
if rstate in release_log["__RSTATES__"]:
release_log["__RSTATES__"][rstate].append("* %s" % l_rstate[end + 1:].strip())
return release_log
def write_release_log(path, release_log, c, cat, rstate, rstate_list):
import io
main_cat, sub_cats = BUGFIX_CATEGORIES[cat[0]]
sub_cat = sub_cats[cat[1]] if cat[1] is not None else None
main_cat_data, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
unreported = [False]
entry = gen_commit_pretty(c, unreported, rstate)
if unreported[0]:
main_cat_data_unreported.setdefault(sub_cat, []).append(entry)
release_log["__COUNT__"][1] += 1
else:
main_cat_data.setdefault(sub_cat, []).append(entry)
release_log["__COUNT__"][0] += 1
if rstate in release_log["__RSTATES__"]:
release_log["__RSTATES__"][rstate].append(gen_commit_pretty(c))
lines = []
main_cat_lines = []
sub_cat_lines = []
for main_cat, sub_cats in BUGFIX_CATEGORIES:
main_cat_data = release_log.get(main_cat, ({}, {}))
main_cat_lines[:] = ["== %s ==" % main_cat]
for data in main_cat_data:
entries = data.get(None, [])
if entries:
main_cat_lines.extend(entries)
main_cat_lines.append("")
if len(main_cat_lines) == 1:
main_cat_lines.append("")
for sub_cat in sub_cats:
sub_cat_lines[:] = ["=== %s ===" % sub_cat]
for data in main_cat_data:
entries = data.get(sub_cat, [])
if entries:
sub_cat_lines.extend(entries)
sub_cat_lines.append("")
if len(sub_cat_lines) > 2:
main_cat_lines += sub_cat_lines
if len(main_cat_lines) > 2:
lines += main_cat_lines
if None in release_log:
main_cat_data = release_log.get(None, ({}, {}))
main_cat_lines[:] = ["== %s ==\n\n" % "UNSORTED"]
for data in main_cat_data:
entries = data.get(None, [])
if entries:
main_cat_lines.extend(entries)
main_cat_lines.append("")
if len(main_cat_lines) > 2:
lines += main_cat_lines
with open(path, 'w') as f:
f.write(release_log["__HEADER__"])
count = release_log["__COUNT__"]
f.write("%s\n" % IGNORE_START_LINE)
f.write("Total fixed bugs: %d (%d from tracker, %d reported/found by other ways).\n\n"
"" % (sum(count), count[0], count[1]))
f.write("%s\n%s\n\n" % ("{{Note|Note|Before RC1 (i.e. during regular development of next version in main "
"branch), only fixes of issues which already existed in previous official releases are "
"listed here. Fixes for regressions introduced since last release, or for new "
"features, are '''not''' listed here.<br/>For following RCs and final release, "
"'''all''' backported fixes are listed.}}", IGNORE_END_LINE))
f.write("\n".join(lines))
f.write("\n")
f.write("%s\n\n<hr/>\n\n" % IGNORE_START_LINE)
for rst in rstate_list:
entries = release_log["__RSTATES__"].get(rst, [])
if entries:
f.write("== %s ==\n" % rst)
f.write("For %s, %d bugs were fixed:\n\n" % (rst, len(entries)))
f.write("\n".join(entries))
f.write("\n\n")
f.write("%s\n" % IGNORE_END_LINE)
def argparse_create():
import argparse
global __doc__
# When --help or no args are given, print this help
usage_text = __doc__
epilog = "This script is typically used to help write release notes"
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter)
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=False,
help="Range to use, eg: 169c95b8..HEAD")
parser.add_argument(
"--author", dest="author",
metavar='AUTHOR', type=str, required=False,
help=("Author(s) to filter commits ("))
parser.add_argument(
"--filter", dest="filter_type",
metavar='FILTER', type=str, required=False,
help=("Method to filter commits in ['BUGFIX', 'NOISE']"))
parser.add_argument(
"--filter-exclude-sha1", dest="filter_exclude_sha1_list",
default=[], required=False, type=lambda s: s.split(","),
help=("Coma-separated list of commits to ignore/skip"))
parser.add_argument(
"--filter-exclude-sha1-fromfiles", dest="filter_exclude_sha1_filepaths",
default="", required=False, nargs='*',
help=("One or more text files storing list of commits to ignore/skip"))
parser.add_argument(
"--filter-exclude-fromreleaselogs", dest="filter_exclude_releaselogs",
default="", required=False, nargs='*',
help=("One or more text files storing release logs, to ignore/skip their entries "
"(based on message comparison, not commit sha1)"))
parser.add_argument(
"--accept-log", dest="accept_log",
default=False, action='store_true', required=False,
help=("Also output more complete info about accepted commits (summary, author...)"))
parser.add_argument(
"--accept-pretty", dest="accept_pretty",
default=False, action='store_true', required=False,
help=("Also output pretty-printed accepted commits (nearly ready for WIKI release notes)"))
parser.add_argument(
"--accept-releaselog", dest="accept_releaselog",
default=False, action='store_true', required=False,
help=("Also output accepted commits as a wiki release log page (adds sorting by categories)"))
parser.add_argument(
"--blender-rev", dest="blender_rev",
default=None, required=False,
help=("Blender revision (only used to generate release notes page)"))
parser.add_argument(
"--blender-rstate", dest="blender_rstate",
default="alpha", required=False,
help=("Blender release state (like alpha, beta, rc1, final, corr_a, corr_b, etc.), "
"each revision will be tagged by given one"))
parser.add_argument(
"--blender-rstate-list", dest="blender_rstate_list",
default="", required=False, type=lambda s: s.split(","),
help=("Blender release state(s) to additionally list in their own sections "
"(e.g. pass 'RC2' to list fixes between RC1 and RC2, ie tagged as RC2, etc.)"))
return parser
def main():
# ----------
# Parse Args
args = argparse_create().parse_args()
for path in args.filter_exclude_sha1_filepaths:
if os.path.exists(path):
with open(path, 'r') as f:
args.filter_exclude_sha1_list += [sha1 for l in f for sha1 in l.split()]
args.filter_exclude_sha1_list = {sha1.encode() for sha1 in args.filter_exclude_sha1_list}
messages = set()
for path in args.filter_exclude_releaselogs:
messages |= release_log_extract_messages(path)
args.filter_exclude_releaselogs = messages
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.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
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
# commits to exclude
if c.sha1 in args.filter_exclude_sha1_list:
return False
# exclude by commit message (because cherry-pick totally breaks relations with original commit...)
if args.filter_exclude_releaselogs:
if gen_commit_message_pretty(c) in args.filter_exclude_releaselogs:
return False
return True
if args.accept_releaselog:
blender_rev = args.blender_rev or "<UNKNOWN>"
commits = tuple(GitCommitIter(args.source_dir, args.range_sha1))
release_log = release_log_init(ACCEPT_RELEASELOG_FILE, args.source_dir, blender_rev,
commits[-1].sha1.decode(), commits[0].sha1.decode(),
args.blender_rstate, args.blender_rstate_list)
commits = [c for c in commits if match(c)]
else:
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(ACCEPT_LOG_FILE, color='yellow'), "(%d)" % tot_accept,
colorize(ACCEPT_PRETTY_FILE, color='blue'), "(%d)" % tot_accept,
colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject,
)
def get_cat(ch, max_idx):
cat = -1
try:
cat = int(ch)
except:
pass
if 0 <= cat < max_idx:
return cat
print("Invalid input %r" % ch)
return None
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()
accept = False
while True:
print("Space=" + colorize("Accept", 'green'),
"Enter=" + colorize("Skip", 'red'),
"Ctrl+C or X=" + colorize("Exit", color='white'),
"[%d of %d]" % (i + 1, len(commits)),
"(+%d | -%d)" % (tot_accept, tot_reject),
)
ch = getch()
if ch == b'\x03' or ch == b'x':
# Ctrl+C
exit_message()
print("Goodbye! (%s)" % c.sha1.decode())
return False
elif ch == b' ':
log_filepath = ACCEPT_FILE
log_filepath_log = ACCEPT_LOG_FILE
log_filepath_pretty = ACCEPT_PRETTY_FILE
tot_accept += 1
if args.accept_releaselog: # Enter sub-loop for category selection.
done_main = True
c1 = c2 = None
while True:
if c1 is None:
print("Select main category (V=View all categories, M=Commit message): \n\t%s"
"" % " | ".join("[%d] %s" % (i, cat[0]) for i, cat in enumerate(BUGFIX_CATEGORIES)))
else:
main_cat = BUGFIX_CATEGORIES[c1][0]
sub_cats = BUGFIX_CATEGORIES[c1][1]
if not sub_cats:
break
print("[%d] %s: Select sub category "
"(V=View all categories, M=Commit message, Enter=No sub-categories, "
"Backspace=Select other main category): \n\t%s"
"" % (c1, main_cat,
" | ".join("[%d] %s" % (i, cat) for i, cat in enumerate(sub_cats))))
ch = getch()
if ch == b'\x7f': # backspace
done_main = False
break
elif ch == b'\x03' or ch == b'x':
# Ctrl+C
exit_message()
print("Goodbye! (%s)" % c.sha1.decode())
return
elif ch == b'v':
print_categories_tree()
print("")
elif ch == b'm':
print_commit(c)
print("")
elif c1 is None:
c1 = get_cat(ch, len(BUGFIX_CATEGORIES))
elif c2 is None:
if ch == b'\r':
break
elif ch == b'\x7f': # backspace
c1 = None
continue
c2 = get_cat(ch, len(BUGFIX_CATEGORIES[c1][1]))
if c2 is not None:
break
else:
print("BUG! this should not happen!")
if done_main is False:
# Go back to main loop, this commit is no more accepted nor rejected.
tot_accept -= 1
continue
write_release_log(ACCEPT_RELEASELOG_FILE, release_log, c, (c1, c2),
args.blender_rstate, args.blender_rstate_list)
break
elif ch == b'\r':
log_filepath = REJECT_FILE
log_filepath_log = None
log_filepath_pretty = None
tot_reject += 1
break
else:
print("Invalid input %r" % ch)
with open(log_filepath, 'ab') as f:
f.write(sha1 + b'\n')
if args.accept_pretty and log_filepath_pretty:
with open(log_filepath_pretty, 'a') as f:
f.write(gen_commit_pretty(c, rstate=args.blender_rstate) + "\n")
if args.accept_log and log_filepath_log:
with open(log_filepath_log, 'a') as f:
f.write(gen_commit_log(c) + "\n")
exit_message()
if __name__ == "__main__":
main()