Re-design of submodules used in blender.git

This commit implements described in the #104573.

The goal is to fix the confusion of the submodule hashes change, which are not
ideal for any of the supported git-module configuration (they are either always
visible causing confusion, or silently staged and committed, also causing
confusion).

This commit replaces submodules with a checkout of addons and addons_contrib,
covered by the .gitignore, and locale and developer tools are moved to the
main repository.

This also changes the paths:
- /release/scripts are moved to the /scripts
- /source/tools are moved to the /tools
- /release/datafiles/locale is moved to /locale

This is done to avoid conflicts when using bisect, and also allow buildbot to
automatically "recover" wgen building older or newer branches/patches.

Running `make update` will initialize the local checkout to the changed
repository configuration.

Another aspect of the change is that the make update will support Github style
of remote organization (origin remote pointing to thy fork, upstream remote
pointing to the upstream blender/blender.git).

Pull Request #104755
This commit is contained in:
Sergey Sharybin
2023-02-21 16:39:58 +01:00
committed by Sergey Sharybin
parent 3e721195b0
commit 03806d0b67
570 changed files with 2039093 additions and 211 deletions
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
import subprocess
import os
from os.path import join
from autopep8_clean_config import PATHS, PATHS_EXCLUDE
from typing import (
Callable,
Generator,
Optional,
Sequence,
)
# Useful to disable when debugging warnings.
USE_MULTIPROCESS = True
print(PATHS)
SOURCE_EXT = (
# Python
".py",
)
def is_source_and_included(filename: str) -> bool:
return (
filename.endswith(SOURCE_EXT) and
filename not in PATHS_EXCLUDE
)
def path_iter(
path: str,
filename_check: Optional[Callable[[str], bool]] = None,
) -> Generator[str, None, None]:
for dirpath, dirnames, filenames in os.walk(path):
# skip ".git"
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if filename.startswith("."):
continue
filepath = join(dirpath, filename)
if filename_check is None or filename_check(filepath):
yield filepath
def path_expand(
paths: Sequence[str],
filename_check: Optional[Callable[[str], bool]] = None,
) -> Generator[str, None, None]:
for f in paths:
if not os.path.exists(f):
print("Missing:", f)
elif os.path.isdir(f):
yield from path_iter(f, filename_check)
else:
yield f
def autopep8_format_file(f: str) -> None:
print(f)
subprocess.call((
"autopep8",
"--ignore",
",".join((
# Info: Use `isinstance()` instead of comparing types directly.
# Why disable?: Changes code logic, in rare cases we want to compare exact types.
"E721",
# Info: Fix bare except.
# Why disable?: Disruptive, leave our exceptions alone.
"E722",
# Info: Fix module level import not at top of file.
# Why disable?: re-ordering imports is disruptive and breaks some scripts
# that need to check if a module has already been loaded in the case of reloading.
"E402",
# Info: Try to make lines fit within --max-line-length characters.
# Why disable? Causes lines to be wrapped, where long lines have the
# trailing bracket moved to the end of the line.
# If trailing commas were respected as they are by clang-format this might be acceptable.
# Note that this doesn't disable all line wrapping.
"E501",
# Info: Fix various deprecated code (via lib2to3)
# Why disable?: causes imports to be added/re-arranged.
"W690",
)),
"--aggressive",
"--in-place",
"--max-line-length", "120",
f,
))
def main() -> None:
import sys
if os.path.samefile(sys.argv[-1], __file__):
paths = path_expand(PATHS, is_source_and_included)
else:
paths = path_expand(sys.argv[1:], is_source_and_included)
if USE_MULTIPROCESS:
import multiprocessing
job_total = multiprocessing.cpu_count()
pool = multiprocessing.Pool(processes=job_total * 2)
pool.map(autopep8_format_file, paths)
else:
for f in paths:
autopep8_format_file(f)
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import os
from typing import (
Generator,
Callable,
Set,
Tuple,
)
PATHS: Tuple[str, ...] = (
"build_files",
"doc",
"release/datafiles",
"release/lts",
"release/scripts/freestyle",
"release/scripts/modules",
"release/scripts/presets",
"release/scripts/startup",
"release/scripts/templates_py",
"source/blender",
"tools",
"tests",
)
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", ".."))))
PATHS = tuple(
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
for p in PATHS
)
PATHS_EXCLUDE: Set[str] = set(
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
for p in
(
"tools/svn_rev_map/sha1_to_rev.py",
"tools/svn_rev_map/rev_to_sha1.py",
"tools/svn_rev_map/rev_to_sha1.py",
"release/scripts/modules/rna_manual_reference.py",
)
)
def files(path: str, test_fn: Callable[[str], bool]) -> Generator[str, None, None]:
for dirpath, dirnames, filenames in os.walk(path):
# skip '.git'
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for filename in filenames:
if test_fn(filename):
filepath = os.path.join(dirpath, filename)
yield filepath
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2015 Blender Foundation - Bastien Montagne.
"""
This is a tool for generating a JSon version of a blender file (only its structure, or all its data included).
It can also run some simple validity checks over a .blend file.
WARNING! This is still WIP tool!
Example usage:
./blend2json.py foo.blend
To output also all 'name' fields from data:
./blend2json.py --filter-data="name" foo.blend
To output complete DNA struct info:
./blend2json.py --full-dna foo.blend
To avoid getting all 'uid' old addresses (those will change really often even when data itself does not change,
making diff pretty noisy):
./blend2json.py --no-old-addresses foo.blend
To check a .blend file instead of outputting its JSon version (use explicit -o option to do both at the same time):
./blend2json.py -c foo.blend
"""
FILTER_DOC = """
Each generic filter is made of three arguments, the include/exclude toggle ('+'/'-'), a regex to match against the name
of the field to check (either one of the 'meta-data' generated by json exporter, or actual data field from DNA structs),
and some regex to match against the data of this field (JSON-ified representation of the data, hence always a string).
Filters are evaluated in the order they are given, that is, if a block does not pass the first filter,
it is immediately rejected and no further check is done on it.
You can add some recursivity to a filter (that is, if an 'include' filter is successful over a 'pointer' property,
it will also automatically include pointed data, with a level of recursivity), by adding either
'*' (for infinite recursion) or a number (to specify the maximum level of recursion) to the include/exclude toggle.
Note that it only makes sense in 'include' case, and gets ignored for 'exclude' one.
Examples:
To include only MESH blocks:
./blend2json.py --filter-block "+" "code" "ME" foo.blend
To include only MESH or CURVE blocks and all data used by them:
./blend2json.py --filter-block "+" "code" "(ME)|(CU)" --filter-block "+*" ".*" ".*" foo.blend
"""
import os
import json
import re
# Avoid maintaining multiple blendfile modules
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
del sys
import blendfile
##### Utils (own json formatting) #####
def json_default(o):
if isinstance(o, bytes):
return repr(o)[2:-1]
elif i is ...:
return "<...>"
return o
def json_dumps(i):
return json.dumps(i, default=json_default)
def keyval_to_json(kvs, indent, indent_step, compact_output=False):
if compact_output:
return ('{' + ', '.join('"%s": %s' % (k, v) for k, v in kvs) + '}')
else:
return ('{%s' % indent_step[:-1] +
(',\n%s%s' % (indent, indent_step)).join(
('"%s":\n%s%s%s' % (k, indent, indent_step, v) if (v[0] in {'[', '{'}) else
'"%s": %s' % (k, v)) for k, v in kvs) +
'\n%s}' % indent)
def list_to_json(lst, indent, indent_step, compact_output=False):
if compact_output:
return ('[' + ', '.join(l for l in lst) + ']')
else:
return ('[%s' % indent_step[:-1] +
((',\n%s%s' % (indent, indent_step)).join(
('\n%s%s%s' % (indent, indent_step, l) if (i == 0 and l[0] in {'[', '{'}) else l)
for i, l in enumerate(lst))
) +
'\n%s]' % indent)
##### Main 'struct' writers #####
def gen_fake_addresses(args, blend):
if args.use_fake_address:
hashes = set()
ret = {}
for block in blend.blocks:
if not block.addr_old:
continue
hsh = block.get_data_hash()
while hsh in hashes:
hsh += 1
hashes.add(hsh)
ret[block.addr_old] = hsh
return ret
return {}
def bheader_to_json(args, fw, blend, indent, indent_step):
fw('%s"%s": [\n' % (indent, "HEADER"))
indent = indent + indent_step
keyval = (
("magic", json_dumps(blend.header.magic)),
("pointer_size", json_dumps(blend.header.pointer_size)),
("is_little_endian", json_dumps(blend.header.is_little_endian)),
("version", json_dumps(blend.header.version)),
)
keyval = keyval_to_json(keyval, indent, indent_step)
fw('%s%s' % (indent, keyval))
indent = indent[:-len(indent_step)]
fw('\n%s]' % indent)
def do_bblock_filter(filters, blend, block, meta_keyval, data_keyval):
def do_bblock_filter_data_recursive(blend, block, rec_lvl, rec_iter, key=None):
fields = (blend.structs[block.sdna_index].fields if key is None else
[blend.structs[block.sdna_index].field_from_name.get(key[1:-1].encode())])
for fld in fields:
if fld is None:
continue
if fld.dna_name.is_pointer:
paths = ([(fld.dna_name.name_only, i) for i in range(fld.dna_name.array_size)]
if fld.dna_name.array_size > 1 else [fld.dna_name.name_only])
for p in paths:
child_block = block.get_pointer(p)
if child_block is not None:
child_block.user_data = max(block.user_data, rec_iter)
if rec_lvl != 0:
do_bblock_filter_data_recursive(blend, child_block, rec_lvl - 1, rec_iter + 1)
has_include = False
do_break = False
rec_iter = 1
if block.user_data is None:
block.user_data = 0
for include, rec_lvl, key, val in filters:
if rec_lvl < 0:
rec_lvl = 100
has_include = has_include or include
# Skip exclude filters if block was already processed some way.
if not include and block.user_data is not None:
continue
has_match = False
for k, v in meta_keyval:
if key.search(k) and val.search(v):
has_match = True
if include:
block.user_data = max(block.user_data, rec_iter)
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
# ones may still have to be processed...
else:
block.user_data = min(block.user_data, -rec_iter)
do_break = True # No need to check more filters in exclude case...
break
for k, v in data_keyval:
if key.search(k) and val.search(v):
has_match = True
if include:
block.user_data = max(block.user_data, rec_iter)
if rec_lvl != 0:
do_bblock_filter_data_recursive(blend, block, rec_lvl - 1, rec_iter + 1, k)
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
# ones may still have to be processed...
else:
block.user_data = min(block.user_data, -rec_iter)
do_break = True # No need to check more filters in exclude case...
break
if include and not has_match: # Include check failed, implies exclusion.
block.user_data = min(block.user_data, -rec_iter)
do_break = True # No need to check more filters in exclude case...
if do_break:
break
# Implicit 'include all' in case no include filter is specified...
if block.user_data == 0 and not has_include:
block.user_data = max(block.user_data, rec_iter)
def bblocks_to_json(args, fw, blend, address_map, indent, indent_step):
no_address = args.no_address
full_data = args.full_data
filter_data = args.filter_data
def gen_meta_keyval(blend, block):
keyval = [
("code", json_dumps(block.code)),
("size", json_dumps(block.size)),
]
if not no_address:
keyval += [("addr_old", json_dumps(address_map.get(block.addr_old, block.addr_old)))]
keyval += [
("dna_type_id", json_dumps(blend.structs[block.sdna_index].dna_type_id)),
("count", json_dumps(block.count)),
]
return keyval
def gen_data_keyval(blend, block, key_filter=None):
def _is_pointer(k):
return blend.structs[block.sdna_index].field_from_path(blend.header, blend.handle, k).dna_name.is_pointer
if key_filter is not None:
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
for k, v in block.items_recursive_iter() if k in key_filter]
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
for k, v in block.items_recursive_iter()]
if args.block_filters:
for block in blend.blocks:
meta_keyval = gen_meta_keyval(blend, block)
data_keyval = gen_data_keyval(blend, block)
do_bblock_filter(args.block_filters, blend, block, meta_keyval, data_keyval)
fw('%s"%s": [\n' % (indent, "DATA"))
indent = indent + indent_step
is_first = True
for i, block in enumerate(blend.blocks):
if block.user_data is None or block.user_data > 0:
meta_keyval = gen_meta_keyval(blend, block)
if full_data:
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block),
indent + indent_step, indent_step, args.compact_output)))
elif filter_data:
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block, filter_data),
indent + indent_step, indent_step, args.compact_output)))
keyval = keyval_to_json(meta_keyval, indent, indent_step, args.compact_output)
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
is_first = False
indent = indent[:-len(indent_step)]
fw('\n%s]' % indent)
def bdna_to_json(args, fw, blend, indent, indent_step):
full_dna = args.full_dna and not args.compact_output
def bdna_fields_to_json(blend, dna, indent, indent_step):
lst = []
for i, field in enumerate(dna.fields):
keyval = (
("dna_name", json_dumps(field.dna_name.name_only)),
("dna_type_id", json_dumps(field.dna_type.dna_type_id)),
("is_pointer", json_dumps(field.dna_name.is_pointer)),
("is_method_pointer", json_dumps(field.dna_name.is_method_pointer)),
("array_size", json_dumps(field.dna_name.array_size)),
)
lst.append(keyval_to_json(keyval, indent + indent_step, indent_step))
return list_to_json(lst, indent, indent_step)
fw('%s"%s": [\n' % (indent, "DNA_STRUCT"))
indent = indent + indent_step
is_first = True
for dna in blend.structs:
keyval = [
("dna_type_id", json_dumps(dna.dna_type_id)),
("size", json_dumps(dna.size)),
]
if full_dna:
keyval += [("fields", bdna_fields_to_json(blend, dna, indent + indent_step, indent_step))]
else:
keyval += [("nbr_fields", json_dumps(len(dna.fields)))]
keyval = keyval_to_json(keyval, indent, indent_step, args.compact_output)
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
is_first = False
indent = indent[:-len(indent_step)]
fw('\n%s]' % indent)
def blend_to_json(args, f, blend, address_map):
fw = f.write
fw('{\n')
indent = indent_step = " "
bheader_to_json(args, fw, blend, indent, indent_step)
fw(',\n')
bblocks_to_json(args, fw, blend, address_map, indent, indent_step)
fw(',\n')
bdna_to_json(args, fw, blend, indent, indent_step)
fw('\n}\n')
##### Checks #####
def check_file(args, blend):
addr_old = set()
for block in blend.blocks:
if block.addr_old in addr_old:
print("ERROR! Several data blocks share same 'addr_old' uuid %d, "
"this should never happen!" % block.addr_old)
continue
addr_old.add(block.addr_old)
##### Main #####
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 check differences between .blend files, or to check their validity."
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
dest="input", nargs="+", metavar='PATH',
help="Input .blend file(s)")
parser.add_argument(
"-o", "--output", dest="output", action="append", metavar='PATH', required=False,
help="Output .json file(s) (same path/name as input file(s) if not specified)")
parser.add_argument(
"-c", "--check-file", dest="check_file", default=False, action='store_true', required=False,
help=("Perform some basic validation checks over the .blend file"))
parser.add_argument(
"--compact-output", dest="compact_output", default=False, action='store_true', required=False,
help=("Output a very compact representation of blendfile (one line per block/DNAStruct)"))
parser.add_argument(
"--no-old-addresses", dest="no_address", default=False, action='store_true', required=False,
help=("Do not output old memory address of each block of data "
"(used as 'uuid' in .blend files, but change pretty noisily)"))
parser.add_argument(
"--no-fake-old-addresses", dest="use_fake_address", default=True, action='store_false',
required=False,
help=("Do not 'rewrite' old memory address of each block of data "
"(they are rewritten by default to some hash of their content, "
"to try to avoid too much diff noise between different but similar files)"))
parser.add_argument(
"--full-data", dest="full_data",
default=False, action='store_true', required=False,
help=("Also put in JSon file data itself "
"(WARNING! will generate *huge* verbose files - and is far from complete yet)"))
parser.add_argument(
"--filter-data", dest="filter_data",
default=None, required=False,
help=("Only put in JSon file data fields which names match given comma-separated list "
"(ignored if --full-data is set)"))
parser.add_argument(
"--full-dna", dest="full_dna", default=False, action='store_true', required=False,
help=("Also put in JSon file dna properties description (ignored when --compact-output is used)"))
group = parser.add_argument_group("Filters", FILTER_DOC)
group.add_argument(
"--filter-block", dest="block_filters", nargs=3, action='append',
help=("Filter to apply to BLOCKS (a.k.a. data itself)"))
return parser
def main():
# ----------
# Parse Args
args = argparse_create().parse_args()
if not args.output:
if args.check_file:
args.output = [None] * len(args.input)
else:
args.output = [os.path.splitext(infile)[0] + ".json" for infile in args.input]
if args.block_filters:
args.block_filters = [(True if m[0] == "+" else False,
0 if len(m) == 1 else (-1 if m[1] == "*" else int(m[1:])),
re.compile(f), re.compile(d))
for m, f, d in args.block_filters]
if args.filter_data:
if args.full_data:
args.filter_data = None
else:
args.filter_data = {n.encode() for n in args.filter_data.split(',')}
for infile, outfile in zip(args.input, args.output):
with blendfile.open_blend(infile) as blend:
address_map = gen_fake_addresses(args, blend)
if args.check_file:
check_file(args, blend)
if outfile:
with open(outfile, 'w', encoding="ascii", errors='xmlcharrefreplace') as f:
blend_to_json(args, f, blend, address_map)
if __name__ == "__main__":
main()
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
r"""
This script exports permutations of key-maps with different settings modified.
Useful for checking changes intended for one configuration don't impact others accidentally.
./blender.bin -b --factory-startup \
--python tools/utils/blender_keyconfig_export_permutations.py -- \
--preset=Blender \
--output-dir=./output \
--keymap-prefs=select_mouse:rmb_action
/blender.bin -b --factory-startup \
--python tools/utils/blender_keyconfig_export_permutations.py -- \
--preset=Blender_27x \
--output-dir=output \
--keymap-prefs="select_mouse"
The preferences setting: ``select_mouse:rmb_action`` expands into:
config = [
("select_mouse", ('LEFT', 'RIGHT')),
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
]
"""
import os
import sys
def argparse_create():
import argparse
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--preset",
dest="preset",
default="Blender",
metavar='PRESET', type=str,
help="The name of the preset to export",
required=False,
)
parser.add_argument(
"--output-dir",
dest="output_dir",
default=".",
metavar='OUTPUT_DIR', type=str,
help="The directory to output to.",
required=False,
)
parser.add_argument(
"--keymap-prefs",
dest="keymap_prefs",
default="select_mouse:rmb_action",
metavar='KEYMAP_PREFS', type=str,
help=(
"Colon separated list of attributes to generate key-map configuration permutations. "
"WARNING: as all combinations are tested, their number increases exponentially!"
),
required=False,
)
return parser
def permutations_from_attrs_impl(config, permutation, index):
index_next = index + 1
attr, values = config[index]
for val in values:
permutation[index] = (attr, val)
if index_next == len(config):
yield tuple(permutation)
else:
# Keep walking down the list of permutations.
yield from permutations_from_attrs_impl(config, permutation, index_next)
# Not necessary, just ensure stale values aren't used.
permutation[index] = None
def permutations_from_attrs(config):
"""
Take a list of attributes and possible values:
config = [
("select_mouse", ('LEFT', 'RIGHT')),
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
]
Yielding all permutations:
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')],
[("select_mouse", 'LEFT'), ("rmb_action", 'FALLBACK_TOOL')],
... etc ...
"""
if not config:
return ()
permutation = [None] * len(config)
result = list(permutations_from_attrs_impl(config, permutation, 0))
assert permutation == ([None] * len(config))
return result
def permutation_as_filename(preset, values):
"""
Takes a configuration, eg:
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')]
And returns a filename compatible path:
"""
from urllib.parse import quote
if not values:
return quote(preset)
return quote(
preset + "_" + ".".join([
"-".join((str(key), str(val)))
for key, val in values
]),
# Needed so forward slashes aren't included in the resulting name.
safe="",
)
def main():
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
try:
import bpy
except ImportError:
# Run outside of Blender, just show "--help".
bpy = None
args.insert(0, "--help")
args = argparse_create().parse_args(args)
if bpy is None:
return
from bpy import context
preset = args.preset
output_dir = args.output_dir
os.makedirs(output_dir, exist_ok=True)
# Needed for background mode.
preset_filepath = bpy.utils.preset_find(preset, preset_path="keyconfig")
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
# Key-map preferences..
km_prefs = context.window_manager.keyconfigs.active.preferences
config = []
# Use RNA introspection:
if args.keymap_prefs:
for attr in args.keymap_prefs.split(":"):
if not hasattr(km_prefs, attr):
print(f"KeyMap preferences does not have attribute: {attr:s}")
sys.exit(1)
prop_def = km_prefs.rna_type.properties.get(attr)
match prop_def.type:
case 'ENUM':
value = tuple(val.identifier for val in prop_def.enum_items)
case 'BOOLEAN':
value = (True, False)
case _ as prop_def_type:
raise Exception(f"Unhandled attribute type {prop_def_type:s}")
config.append((attr, value))
config = tuple(config)
for attr_permutation in (permutations_from_attrs(config) or ((),)):
# Reload and set.
if attr_permutation is not None:
km_prefs = context.window_manager.keyconfigs.active.preferences
for attr, value in attr_permutation:
setattr(km_prefs, attr, value)
# Re-activate after setting preferences, tsk, ideally this shouldn't be necessary.
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
filepath = os.path.join(output_dir, permutation_as_filename(preset, attr_permutation) + ".py")
print("Writing:", filepath)
bpy.ops.preferences.keyconfig_export(filepath=filepath, all=True)
sys.exit()
if __name__ == "__main__":
main()
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import subprocess
import sys
# We unfortunately ended with three commits instead of a single one to be handled as
# 'clang-format' commit, we are handling them as a single 'block'.
format_commits = (
'e12c08e8d170b7ca40f204a5b0423c23a9fbc2c1',
'91a9cd0a94000047248598394c41ac30f893f147',
'3076d95ba441cd32706a27d18922a30f8fd28b8a',
)
pre_format_commit = format_commits[0] + '~1'
def get_string(cmd):
return subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode('utf8').strip()
# Parse arguments.
mode = None
base_branch = 'main'
if len(sys.argv) >= 2:
# Note that recursive conflict resolution strategy has to reversed in rebase compared to merge.
# See https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt--m
if sys.argv[1] == '--rebase':
mode = 'rebase'
recursive_format_commit_merge_options = '-Xignore-all-space -Xtheirs'
elif sys.argv[1] == '--merge':
mode = 'merge'
recursive_format_commit_merge_options = '-Xignore-all-space -Xours'
if len(sys.argv) == 4:
if sys.argv[2] == '--base_branch':
base_branch = sys.argv[3]
if mode is None:
print("Merge or rebase Blender main (or another base branch) into a branch in 3 steps,")
print("to automatically merge clang-format changes.")
print("")
print(" --rebase Perform equivalent of 'git rebase main'")
print(" --merge Perform equivalent of 'git merge main'")
print("")
print("Optional arguments:")
print(" --base_branch <branch name> Use given branch instead of main")
print(" (assuming that base branch has already been updated")
print(" and has the initial clang-format commit).")
sys.exit(0)
# Verify we are in the right directory.
root_path = get_string(['git', 'rev-parse', '--show-superproject-working-tree'])
if os.path.realpath(root_path) != os.path.realpath(os.getcwd()):
print("BLENDER MERGE: must run from blender repository root directory")
sys.exit(1)
# Abort if a rebase is still progress.
rebase_merge = get_string(['git', 'rev-parse', '--git-path', 'rebase-merge'])
rebase_apply = get_string(['git', 'rev-parse', '--git-path', 'rebase-apply'])
merge_head = get_string(['git', 'rev-parse', '--git-path', 'MERGE_HEAD'])
if os.path.exists(rebase_merge) or \
os.path.exists(rebase_apply) or \
os.path.exists(merge_head):
print("BLENDER MERGE: rebase or merge in progress, complete it first")
sys.exit(1)
# Abort if uncommitted changes.
changes = get_string(['git', 'status', '--porcelain', '--untracked-files=no'])
if len(changes) != 0:
print("BLENDER MERGE: detected uncommitted changes, can't run")
sys.exit(1)
# Setup command, with commit message for merge commits.
if mode == 'rebase':
mode_cmd = 'rebase'
else:
branch = get_string(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
mode_cmd = 'merge --no-edit -m "Merge \'' + base_branch + '\' into \'' + branch + '\'"'
# Rebase up to the clang-format commit.
code = os.system('git merge-base --is-ancestor ' + pre_format_commit + ' HEAD')
if code != 0:
code = os.system('git ' + mode_cmd + ' ' + pre_format_commit)
if code != 0:
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and run again")
sys.exit(code)
# Rebase clang-format commit.
code = os.system('git merge-base --is-ancestor ' + format_commits[-1] + ' HEAD')
if code != 0:
os.system('git ' + mode_cmd + ' ' + recursive_format_commit_merge_options + ' ' + format_commits[-1])
paths = get_string(('git', '--no-pager', 'diff', '--name-only', format_commits[-1])).replace('\n', ' ')
if sys.platform == 'win32' and len(paths) > 8000:
# Windows command-line does not accept more than 8191 chars.
os.system('make format')
else:
os.system('make format PATHS="' + paths + '"')
os.system('git add -u')
count = int(get_string(['git', 'rev-list', '--count', '' + format_commits[-1] + '..HEAD']))
if count == 1 or mode == 'merge':
# Amend if we just have a single commit or are merging.
os.system('git commit --amend --no-edit')
else:
# Otherwise create a commit for formatting.
os.system('git commit -m "Cleanup: apply clang format"')
# Rebase remaining commits
code = os.system('git ' + mode_cmd + ' ' + base_branch)
if code != 0:
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and you're done")
else:
print("BLENDER MERGE: done")
sys.exit(code)
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Generates 'userdef_default_theme.c' from a 'userpref.blend' file.
Pass your user preferenes blend file to this script to update the C source file.
eg:
./tools/utils/blender_theme_as_c.py ~/.config/blender/2.80/config/userpref.blend
.. or find the latest:
./tools/utils/blender_theme_as_c.py $(find ~/.config/blender -name "userpref.blend" | sort | tail -1)
"""
C_SOURCE_HEADER = r'''/* SPDX-License-Identifier: GPL-2.0-or-later */
/**
* Generated by 'tools/utils/blender_theme_as_c.py'
*
* Do not hand edit this file!
*/
#include "DNA_userdef_types.h"
#include "BLO_readfile.h"
/* clang-format off */
#ifdef __LITTLE_ENDIAN__
# define RGBA(c) {((c) >> 24) & 0xff, ((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
# define RGB(c) {((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
#else
# define RGBA(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff, ((c) >> 24) & 0xff}
# define RGB(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff}
#endif
'''
def round_float_32(f):
from struct import pack, unpack
return unpack("f", pack("f", f))[0]
def repr_f32(f):
f_round = round_float_32(f)
f_str = repr(f)
f_str_frac = f_str.partition(".")[2]
if not f_str_frac:
return f_str
for i in range(1, len(f_str_frac)):
f_test = round(f, i)
f_test_round = round_float_32(f_test)
if f_test_round == f_round:
return "%.*f" % (i, f_test)
return f_str
import os
# Avoid maintaining multiple blendfile modules
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
del sys
source_dst = os.path.join(
os.path.dirname(__file__),
"..", "..", "..",
"release", "datafiles", "userdef", "userdef_default_theme.c",
)
dna_rename_defs_h = os.path.join(
os.path.dirname(__file__),
"..", "..", "..",
"source", "blender", "makesdna", "intern", "dna_rename_defs.h",
)
def dna_rename_defs(blend):
"""
"""
from blendfile import DNAName
import re
re_dna_struct_rename_elem = re.compile(
r'DNA_STRUCT_RENAME_ELEM+\('
r'([a-zA-Z0-9_]+)' r',\s*'
r'([a-zA-Z0-9_]+)' r',\s*'
r'([a-zA-Z0-9_]+)' r'\)',
)
with open(dna_rename_defs_h, 'r', encoding='utf-8') as fh:
data = fh.read()
for l in data.split('\n'):
m = re_dna_struct_rename_elem.match(l)
if m is not None:
struct_name, member_storage, member_runtime = m.groups()
struct_name = struct_name.encode('utf-8')
member_storage = member_storage.encode('utf-8')
member_runtime = member_runtime.encode('utf-8')
dna_struct = blend.structs[blend.sdna_index_from_id[struct_name]]
for field in dna_struct.fields:
dna_name = field.dna_name
if member_storage == dna_name.name_only:
field.dna_name = dna_name = DNAName(dna_name.name_full)
del dna_struct.field_from_name[dna_name.name_only]
dna_name.name_full = dna_name.name_full.replace(member_storage, member_runtime)
dna_name.name_only = member_runtime
dna_struct.field_from_name[dna_name.name_only] = field
def theme_data(userpref_filename):
import blendfile
blend = blendfile.open_blend(userpref_filename)
dna_rename_defs(blend)
u = next((c for c in blend.blocks if c.code == b'USER'), None)
# theme_type = b.sdna_index_from_id[b'bTheme']
t = u.get_pointer((b'themes', b'first'))
t.refine_type(b'bTheme')
return blend, t
def is_ignore_dna_name(name):
if name.startswith(b'_'):
return True
elif name in {
b'active_theme_area',
}:
return True
else:
return False
def write_member(fw, indent, b, theme, ls):
path_old = ()
for key, value in ls:
key = key if type(key) is tuple else (key,)
path_new = key[:-1]
if tuple(path_new) != tuple(path_old):
if path_old:
p = len(path_old) - 1
while p >= 0 and (p >= len(path_new) or path_new[p] != path_old[p]):
indent = p + 1
fw('\t' * indent)
fw('},\n')
p -= 1
del p
p = 0
for p in range(min(len(path_old), len(path_new))):
if path_old[p] != key[p]:
break
else:
p = p + 1
for i, c in enumerate(path_new[p:]):
indent = p + i + 1
fw('\t' * indent)
if type(c) is bytes:
attr = c.decode('ascii')
fw(f'.{attr} = ')
fw('{\n')
if not is_ignore_dna_name(key[-1]):
indent = '\t' * (len(path_new) + 1)
attr = key[-1].decode('ascii')
if isinstance(value, float):
if value != 0.0:
value_repr = repr_f32(value)
fw(f'{indent}.{attr} = {value_repr}f,\n')
elif isinstance(value, int):
if value != 0:
fw(f'{indent}.{attr} = {value},\n')
elif isinstance(value, bytes):
if set(value) != {0}:
if len(value) == 3:
value_repr = "".join(f'{ub:02x}' for ub in value)
fw(f'{indent}.{attr} = RGB(0x{value_repr}),\n')
elif len(value) == 4:
value_repr = "".join(f'{ub:02x}' for ub in value)
fw(f'{indent}.{attr} = RGBA(0x{value_repr}),\n')
else:
value = value.rstrip(b'\x00')
is_ascii = True
for ub in value:
if not (ub >= 32 and ub < 127):
is_ascii = False
break
if is_ascii:
value_repr = value.decode('ascii')
fw(f'{indent}.{attr} = "{value_repr}",\n')
else:
value_repr = "".join(f'{ub:02x}' for ub in value)
fw(f'{indent}.{attr} = {{{value_repr}}},\n')
else:
fw(f'{indent}.{attr} = {value},\n')
path_old = path_new
def convert_data(blend, theme, f):
fw = f.write
fw(C_SOURCE_HEADER)
fw('const bTheme U_theme_default = {\n')
ls = list(theme.items_recursive_iter(use_nil=False))
write_member(fw, 1, blend, theme, ls)
fw('};\n')
fw('\n')
fw('/* clang-format on */\n')
def file_remove_empty_braces(source_dst):
with open(source_dst, 'r', encoding='utf-8') as fh:
data = fh.read()
# Remove:
# .foo = { }
import re
def key_replace(match):
return ""
data_prev = None
# Braces may become empty by removing nested
while data != data_prev:
data_prev = data
data = re.sub(
r'\s+\.[a-zA-Z_0-9]+\s+=\s+\{\s*\},',
key_replace, data, re.MULTILINE,
)
# Use two spaces instead of tabs.
data = data.replace('\t', ' ')
with open(source_dst, 'w', encoding='utf-8') as fh:
fh.write(data)
def main():
import sys
blend, theme = theme_data(sys.argv[-1])
with open(source_dst, 'w', encoding='utf-8') as fh:
convert_data(blend, theme, fh)
# Microsoft Visual Studio doesn't support empty braces.
file_remove_empty_braces(source_dst)
if __name__ == "__main__":
main()
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Example use:
credits_git_gen.py --source=/src/blender --range=SHA1..HEAD
"""
from git_log import GitCommitIter
import unicodedata as ud
# -----------------------------------------------------------------------------
# Lookup Table to clean up the credits
#
# This is a combination of unifying git logs as well as
# name change requested by the authors.
author_table = {
"Aaron": "Aaron Carlisle",
"Your Name": "Aaron Carlisle",
"Alan": "Alan Troth",
"andreas atteneder": "Andreas Atteneder",
"Ankit": "Ankit Meel",
"Antonioya": "Antonio Vazquez",
"Antonio Vazquez": "Antonio Vazquez",
"Antony Ryakiotakis": "Antony Riakiotakis",
"Amélie Fondevilla": "Amelie Fondevilla",
"bastien": "Bastien Montagne",
"mont29": "Bastien Montagne",
"bjornmose": "Bjorn Mose",
"meta-androcto": "Brendon Murphy",
"Brecht van Lommel": "Brecht Van Lommel",
"Brecht Van Lömmel": "Brecht Van Lommel",
"recht Van Lommel": "Brecht Van Lommel",
"Clément Foucault": "Clément Foucault",
"Clément": "Clément Foucault",
"fclem": "Clément Foucault",
"Clment Foucault": "Clément Foucault",
"christian brinkmann": "Christian Brinkmann",
"ZanQdo": "Daniel Salazar",
"unclezeiv": "Davide Vercelli",
"dilithjay": "Dilith Jayakody",
"gaiaclary": "Gaia Clary",
"Diego Hernan Borghetti": "Diego Borghetti",
"Dotsnov Valentin": "Dontsov Valentin",
"Eitan": "Eitan Traurig",
"EitanSomething": "Eitan Traurig",
"Erik": "Erik Abrahamsson",
"Erick Abrahammson": "Erik Abrahamsson",
"Eric Abrahamsson": "Erik Abrahamsson",
"Ethan-Hall": "Ethan Hall",
"filedescriptor": "Falk David",
"Germano": "Germano Cavalcante",
"Germano Cavalcantemano-wii": "Germano Cavalcante",
"mano-wii": "Germano Cavalcante",
"gsr": "Guillermo S. Romero",
"Henrik Dick (weasel)": "Henrik Dick",
"howardt": "Howard Trickey",
"Iliay Katueshenock": "Iliya Katueshenock",
"MOD": "Iliya Katueshenock",
"Inês Almeida": "Ines Almeida",
"brita": "Ines Almeida",
"Ivan": "Ivan Perevala",
"jensverwiebe": "Jens Verwiebe",
"Jesse Y": "Jesse Yurkovich",
"Joe Eagar": "Joseph Eagar",
"Johnny Matthews (guitargeek)": "Johnny Matthews",
"guitargeek": "Johnny Matthews",
"jon denning": "Jon Denning",
"julianeisel": "Julian Eisel",
"Severin": "Julian Eisel",
"Alex Strand": "Kenzie Strand",
"Kevin Dietrich": "Kévin Dietrich",
"Leon Leno": "Leon Schittek",
"Lukas Toenne": "Lukas Tönne",
"Mikhail": "Mikhail Matrosov",
"OmarSquircleArt": "Omar Emara",
"lazydodo": "Ray Molenkamp",
"Ray molenkamp": "Ray Molenkamp",
"Author Name": "Robert Guetzkow",
"Sybren A. Stüvel": "Sybren A. Stüvel",
"Simon": "Simon G",
"Stephan": "Stephan Seitz",
"Sebastian Herhoz": "Sebastian Herholz",
"blender": "Sergey Sharybin",
"Vuk GardaÅ¡ević": "Vuk Gardašević",
"ianwill": "Willian Padovani Germano",
"Yiming Wu": "YimingWu",
}
# -----------------------------------------------------------------------------
# Class for generating credits
class CreditUser:
__slots__ = (
"commit_total",
"year_min",
"year_max",
)
def __init__(self):
self.commit_total = 0
class Credits:
__slots__ = (
"users",
)
def __init__(self):
self.users = {}
def process_commit(self, c):
# Normalize author string into canonical form, prevents duplicate credit users
author = ud.normalize('NFC', c.author)
author = author_table.get(author, author)
year = c.date.year
cu = self.users.get(author)
if cu is None:
cu = self.users[author] = CreditUser()
cu.year_min = year
cu.year_max = year
cu.commit_total += 1
cu.year_min = min(cu.year_min, year)
cu.year_max = max(cu.year_max, year)
def process(self, commit_iter):
for i, c in enumerate(commit_iter):
self.process_commit(c)
if not (i % 100):
print(i)
def write(self, filepath,
is_main_credits=True,
contrib_companies=(),
sort="name"):
# patch_word = "patch", "patches"
commit_word = "commit", "commits"
sorted_authors = {}
if sort == "commit":
sorted_authors = dict(sorted(self.users.items(), key=lambda item: item[1].commit_total))
else:
sorted_authors = dict(sorted(self.users.items()))
with open(filepath, 'w', encoding="ascii", errors='xmlcharrefreplace') as file:
file.write("<h3>Individual Contributors</h3>\n\n")
for author, cu in sorted_authors.items():
file.write("{:s}, {:,d} {:s} {:s}<br />\n".format(
author,
cu.commit_total,
commit_word[cu.commit_total > 1],
("" if not is_main_credits else
("- {:d}".format(cu.year_min) if cu.year_min == cu.year_max else
("({:d} - {:d})".format(cu.year_min, cu.year_max))))))
file.write("\n\n")
# -------------------------------------------------------------------------
# Companies, hard coded
if is_main_credits:
file.write("<h3>Contributions from Companies & Organizations</h3>\n")
file.write("<p>\n")
for line in contrib_companies:
file.write("{:s}<br />\n".format(line))
file.write("</p>\n")
import datetime
now = datetime.datetime.now()
fn = __file__.split("\\")[-1].split("/")[-1]
file.write(
"<p><center><i>Generated by '{:s}' {:d}/{:d}/{:d}</i></center></p>\n".format(
fn, now.year, now.month, now.day
))
def argparse_create():
import argparse
# When --help or no args are given, print this help
usage_text = "Review revisions."
epilog = "This script is used to generate credits"
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(
"--sort", dest="sort",
metavar='METHOD',
required=False,
help="Sort credits by 'name' (default) or 'commit'",
)
return parser
def main():
# ----------
# Parse Args
args = argparse_create().parse_args()
def is_credit_commit_valid(c):
ignore_dir = (
b"blender/extern/",
b"blender/intern/opennl/",
)
if not any(f for f in c.files if not f.startswith(ignore_dir)):
return False
return True
# TODO, there are for sure more companies then are currently listed.
# 1 liners for in html syntax
contrib_companies = (
"<b>Unity Technologies</b> - FBX Exporter",
"<b>BioSkill GmbH</b> - H3D compatibility for X3D Exporter, "
"OBJ Nurbs Import/Export",
"<b>AutoCRC</b> - Improvements to fluid particles, vertex color baking",
"<b>Adidas</b> - Principled BSDF shader in Cycles",
"<b>AMD</b> - Cycles HIP GPU rendering, CPU optimizations",
"<b>Intel</b> - Cycles oneAPI GPU rendering, CPU optimizations",
"<b>NVIDIA</b> - Cycles OptiX GPU rendering, USD importer",
"<b>Facebook</b> - Cycles subsurface scattering improvements",
"<b>Apple</b> - Cycles Metal GPU backend",
)
credits = Credits()
# commit_range = "HEAD~10..HEAD"
# commit_range = "blender-v2.81-release..blender-v2.82-release"
# commit_range = "blender-v2.82-release"
commit_range = args.range_sha1
sort = args.sort
citer = GitCommitIter(args.source_dir, commit_range)
credits.process((c for c in citer if is_credit_commit_valid(c)))
credits.write("credits.html",
is_main_credits=True,
contrib_companies=contrib_companies,
sort=sort)
print("Written: credits.html")
if __name__ == "__main__":
main()
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
import collections
import os
import subprocess
import sys
# Hashes to be ignored
#
# The system sometimes fails to match commits and suggests to backport
# revision which was already ported. In order to solve that we can:
#
# - Explicitly ignore some of the commits.
# - Move the synchronization point forward.
IGNORE_HASHES = {
}
# Start revisions from both repositories.
CYCLES_START_COMMIT = b"b941eccba81bbb1309a0eb4977fc3a77796f4ada" # blender-v2.92
BLENDER_START_COMMIT = b"02948a2cab44f74ed101fc1b2ad9fe4431123e85" # v2.92
# Prefix which is common for all the subjects.
GIT_SUBJECT_COMMON_PREFIX = b"Subject: [PATCH] "
# Marker which indicates begin of new file in the patch set.
GIT_FILE_SECTION_MARKER = b"diff --git"
# Marker of the end of the patchset.
GIT_PATCHSET_END_MARKER = b"-- "
# Prefix of topic to be omitted
SUBJECT_SKIP_PREFIX = (
b"Cycles: ",
b"cycles: ",
b"Cycles Standalone: ",
b"Cycles standalone: ",
b"cycles standalone: ",
)
def subject_strip(common_prefix, subject):
for prefix in SUBJECT_SKIP_PREFIX:
full_prefix = common_prefix + prefix
if subject.startswith(full_prefix):
subject = subject[len(full_prefix):].capitalize()
subject = common_prefix + subject
break
return subject
def replace_file_prefix(path, prefix, replace_prefix):
tokens = path.split(b' ')
prefix_len = len(prefix)
for i, t in enumerate(tokens):
for x in (b"a/", b"b/"):
if t.startswith(x + prefix):
tokens[i] = x + replace_prefix + t[prefix_len + 2:]
return b' '.join(tokens)
def cleanup_patch(patch, accept_prefix, replace_prefix):
assert accept_prefix[0] != b'/'
assert replace_prefix[0] != b'/'
full_accept_prefix = GIT_FILE_SECTION_MARKER + b" a/" + accept_prefix
with open(patch, "rb") as f:
content = f.readlines()
clean_content = []
do_skip = False
for line in content:
if line.startswith(GIT_SUBJECT_COMMON_PREFIX):
# Skip possible prefix like "Cycles:", we already know change is
# about Cycles since it's being committed to a Cycles repository.
line = subject_strip(GIT_SUBJECT_COMMON_PREFIX, line)
# Dots usually are omitted in the topic
line = line.replace(b".\n", b"\n")
elif line.startswith(GIT_FILE_SECTION_MARKER):
if not line.startswith(full_accept_prefix):
do_skip = True
else:
do_skip = False
line = replace_file_prefix(line, accept_prefix, replace_prefix)
elif line.startswith(GIT_PATCHSET_END_MARKER):
do_skip = False
elif line.startswith(b"---") or line.startswith(b"+++"):
line = replace_file_prefix(line, accept_prefix, replace_prefix)
if not do_skip:
clean_content.append(line)
with open(patch, "wb") as f:
f.writelines(clean_content)
# Get mapping from commit subject to commit hash.
#
# It'll actually include timestamp of the commit to the map key, so commits with
# the same subject wouldn't conflict with each other.
def commit_map_get(repository, path, start_commit):
command = (b"git",
b"--git-dir=" + os.path.join(repository, b'.git'),
b"--work-tree=" + repository,
b"log", b"--format=%H %at %s", b"--reverse",
start_commit + b'..HEAD',
b'--',
os.path.join(repository, path),
b':(exclude)' + os.path.join(repository, b'intern/cycles/blender'))
lines = subprocess.check_output(command).split(b"\n")
commit_map = collections.OrderedDict()
for line in lines:
if line:
commit_sha, stamped_subject = line.split(b' ', 1)
stamp, subject = stamped_subject.split(b' ', 1)
subject = subject_strip(b"", subject).rstrip(b".")
stamped_subject = stamp + b" " + subject
if commit_sha in IGNORE_HASHES:
continue
commit_map[stamped_subject] = commit_sha
return commit_map
# Get difference between two lists of commits.
# Returns two lists: first are the commits to be ported from Cycles to Blender,
# second one are the commits to be ported from Blender to Cycles.
def commits_get_difference(cycles_map, blender_map):
cycles_to_blender = []
for stamped_subject, commit_hash in cycles_map.items():
if stamped_subject not in blender_map:
cycles_to_blender.append(commit_hash)
blender_to_cycles = []
for stamped_subject, commit_hash in blender_map.items():
if stamped_subject not in cycles_map:
blender_to_cycles.append(commit_hash)
return cycles_to_blender, blender_to_cycles
# Transfer commits from one repository to another.
# Doesn't do actual commit just for the safety.
def transfer_commits(commit_hashes,
from_repository,
to_repository,
dst_is_cycles):
patch_index = 1
for commit_hash in commit_hashes:
command = (
b"git",
b"--git-dir=" + os.path.join(from_repository, b'.git'),
b"--work-tree=" + from_repository,
b"format-patch", b"-1",
b"--start-number", bytes(str(patch_index), 'utf-8'),
b"-o", to_repository,
commit_hash,
b'--',
b':(exclude)' + os.path.join(from_repository, b'intern/cycles/blender'),
)
patch_file = subprocess.check_output(command).rstrip(b"\n")
if dst_is_cycles:
cleanup_patch(patch_file, b"intern/cycles", b"src")
else:
cleanup_patch(patch_file, b"src", b"intern/cycles")
patch_index += 1
def main():
if len(sys.argv) != 3:
print("Usage: %s /path/to/cycles/ /path/to/blender/" % sys.argv[0])
return
cycles_repository = sys.argv[1].encode()
blender_repository = sys.argv[2].encode()
cycles_map = commit_map_get(cycles_repository, b'', CYCLES_START_COMMIT)
blender_map = commit_map_get(blender_repository, b"intern/cycles", BLENDER_START_COMMIT)
diff = commits_get_difference(cycles_map, blender_map)
transfer_commits(diff[0], cycles_repository, blender_repository, False)
transfer_commits(diff[1], blender_repository, cycles_repository, True)
print("Missing commits were saved to the blender and cycles repositories.")
print("Check them and if they're all fine run:")
print("")
print(" git am *.patch")
if __name__ == '__main__':
main()
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
import argparse
import re
import shutil
import subprocess
import sys
import time
class COLORS:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
VERBOSE = False
#########################################
# Generic helper functions.
def logVerbose(*args):
if VERBOSE:
print(*args)
def logHeader(*args):
print(COLORS.HEADER + COLORS.BOLD, end="")
print(*args, end="")
print(COLORS.ENDC)
def logWarning(*args):
print(COLORS.WARNING + COLORS.BOLD, end="")
print(*args, end="")
print(COLORS.ENDC)
def logOk(*args):
print(COLORS.OKGREEN + COLORS.BOLD, end="")
print(*args, end="")
print(COLORS.ENDC)
def progress(count, total, prefix="", suffix=""):
if VERBOSE:
return
size = shutil.get_terminal_size((80, 20))
if prefix != "":
prefix = prefix + " "
if suffix != "":
suffix = " " + suffix
bar_len = size.columns - len(prefix) - len(suffix) - 10
filled_len = int(round(bar_len * count / float(total)))
percents = round(100.0 * count / float(total), 1)
bar = '=' * filled_len + '-' * (bar_len - filled_len)
sys.stdout.write('%s[%s] %s%%%s\r' % (prefix, bar, percents, suffix))
sys.stdout.flush()
def progressClear():
if VERBOSE:
return
size = shutil.get_terminal_size((80, 20))
sys.stdout.write(" " * size.columns + "\r")
sys.stdout.flush()
def humanReadableTimeDifference(seconds):
hours = int(seconds) // 60 // 60
seconds = seconds - hours * 60 * 60
minutes = int(seconds) // 60
seconds = seconds - minutes * 60
if hours == 0:
return "%02d:%05.2f" % (minutes, seconds)
else:
return "%02d:%02d:%05.2f" % (hours, minutes, seconds)
def humanReadableTimeToSeconds(time):
tokens = time.split(".")
result = 0
if len(tokens) == 2:
result = float("0." + tokens[1])
mult = 1
for token in reversed(tokens[0].split(":")):
result += int(token) * mult
mult *= 60
return result
#########################################
# Benchmark specific helper functions.
def configureArgumentParser():
parser = argparse.ArgumentParser(
description="Cycles benchmark helper script.")
parser.add_argument("-b", "--binary",
help="Full file path to Blender's binary " +
"to use for rendering",
default="blender")
parser.add_argument("-f", "--files", nargs='+')
parser.add_argument("-v", "--verbose",
help="Perform fully verbose communication",
action="store_true",
default=False)
return parser
def benchmarkFile(blender, blendfile, stats):
logHeader("Begin benchmark of file {}" . format(blendfile))
# Prepare some regex for parsing
re_path_tracing = re.compile(".*Path Tracing Tile ([0-9]+)/([0-9]+)$")
re_total_render_time = re.compile(r".*Total render time: ([0-9]+(\.[0-9]+)?)")
re_render_time_no_sync = re.compile(
".*Render time \\(without synchronization\\): ([0-9]+(\\.[0-9]+)?)")
re_pipeline_time = re.compile(r"Time: ([0-9:\.]+) \(Saving: ([0-9:\.]+)\)")
# Prepare output folder.
# TODO(sergey): Use some proper output folder.
output_folder = "/tmp/"
# Configure command for the current file.
command = (blender,
"--background",
"-noaudio",
"--factory-startup",
blendfile,
"--engine", "CYCLES",
"--debug-cycles",
"--render-output", output_folder,
"--render-format", "PNG",
"-f", "1")
# Run Blender with configured command line.
logVerbose("About to execute command: {}" . format(command))
start_time = time.time()
process = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
# Keep reading status while Blender is alive.
total_render_time = "N/A"
render_time_no_sync = "N/A"
pipeline_render_time = "N/A"
while True:
line = process.stdout.readline()
if line == b"" and process.poll() is not None:
break
line = line.decode().strip()
if line == "":
continue
logVerbose("Line from stdout: {}" . format(line))
match = re_path_tracing.match(line)
if match:
current_tiles = int(match.group(1))
total_tiles = int(match.group(2))
elapsed_time = time.time() - start_time
elapsed_time_str = humanReadableTimeDifference(elapsed_time)
progress(current_tiles,
total_tiles,
prefix="Path Tracing Tiles {}" . format(elapsed_time_str))
match = re_total_render_time.match(line)
if match:
total_render_time = float(match.group(1))
match = re_render_time_no_sync.match(line)
if match:
render_time_no_sync = float(match.group(1))
match = re_pipeline_time.match(line)
if match:
pipeline_render_time = humanReadableTimeToSeconds(match.group(1))
if process.returncode != 0:
return False
# Clear line used by progress.
progressClear()
print("Total pipeline render time: {} ({} sec)"
. format(humanReadableTimeDifference(pipeline_render_time),
pipeline_render_time))
print("Total Cycles render time: {} ({} sec)"
. format(humanReadableTimeDifference(total_render_time),
total_render_time))
print("Pure Cycles render time (without sync): {} ({} sec)"
. format(humanReadableTimeDifference(render_time_no_sync),
render_time_no_sync))
logOk("Successfully rendered")
stats[blendfile] = {'PIPELINE_TOTAL': pipeline_render_time,
'CYCLES_TOTAL': total_render_time,
'CYCLES_NO_SYNC': render_time_no_sync}
return True
def benchmarkAll(blender, files):
stats = {}
for blendfile in files:
try:
benchmarkFile(blender, blendfile, stats)
except KeyboardInterrupt:
print("")
logWarning("Rendering aborted!")
return
def main():
parser = configureArgumentParser()
args = parser.parse_args()
if args.verbose:
global VERBOSE
VERBOSE = True
benchmarkAll(args.binary, args.files)
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
# SPDX-License-Identifier: GPL-2.0-or-later
'''
Define the command 'print_struct_c99' for gdb,
useful for creating a literal for a nested runtime struct.
Example use:
(gdb) source tools/utils/gdb_struct_repr_c99.py
(gdb) print_struct_c99 scene->toolsettings
'''
class PrintStructC99(gdb.Command):
def __init__(self):
super(PrintStructC99, self).__init__(
"print_struct_c99",
gdb.COMMAND_USER,
)
def get_count_heading(self, string):
for i, s in enumerate(string):
if s != ' ':
break
return i
def extract_typename(self, string):
first_line = string.split('\n')[0]
return first_line.split('=')[1][:-1].strip()
def invoke(self, arg, from_tty):
ret_ptype = gdb.execute('ptype {}'.format(arg), to_string=True)
tname = self.extract_typename(ret_ptype)
print('{} {} = {{'.format(tname, arg))
r = gdb.execute('p {}'.format(arg), to_string=True)
r = r.split('\n')
for rr in r[1:]:
if '=' not in rr:
print(rr)
continue
hs = self.get_count_heading(rr)
rr_s = rr.strip().split('=', 1)
rr_rval = rr_s[1].strip()
print(' ' * hs + '.' + rr_s[0] + '= ' + rr_rval)
print('Running GDB from: %s\n' % (gdb.PYTHONDIR))
gdb.execute("set print pretty")
gdb.execute('set pagination off')
gdb.execute('set print repeats 0')
gdb.execute('set print elements unlimited')
# instantiate
PrintStructC99()
+191
View File
@@ -0,0 +1,191 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Simple module for inspecting git commits
import os
import subprocess
class GitCommit:
__slots__ = (
"sha1",
# to extract more info
"_git_dir",
# cached values
"_author",
"_date",
"_body",
"_files",
"_files_status",
)
def __init__(self, sha1, git_dir):
self.sha1 = sha1
self._git_dir = git_dir
self._author = \
self._date = \
self._body = \
self._files = \
self._files_status = \
None
def cache(self):
""" Cache all properties
"""
self.author
self.date
self.body
self.files
self.files_status
def _log_format(self, format, args=()):
# sha1 = self.sha1.decode('ascii')
cmd = (
"git",
"--git-dir",
self._git_dir,
"log",
"-1", # only this rev
self.sha1,
"--format=" + format,
) + args
# print(" ".join(cmd))
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
)
return p.stdout.read()
@property
def sha1_short(self):
cmd = (
"git",
"--git-dir",
self._git_dir,
"rev-parse",
"--short",
self.sha1,
)
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
)
return p.stdout.read().strip().decode('ascii')
@property
def author(self):
ret = self._author
if ret is None:
content = self._log_format("%an")[:-1]
ret = content.decode("utf8", errors="ignore")
self._author = ret
return ret
@property
def date(self):
ret = self._date
if ret is None:
import datetime
ret = datetime.datetime.fromtimestamp(int(self._log_format("%ct")))
self._date = ret
return ret
@property
def body(self):
ret = self._body
if ret is None:
content = self._log_format("%B")[:-1]
ret = content.decode("utf8", errors="ignore")
self._body = ret
return ret
@property
def subject(self):
return self.body.lstrip().partition("\n")[0]
@property
def files(self):
ret = self._files
if ret is None:
ret = [f for f in self._log_format("format:", args=("--name-only",)).split(b"\n") if f]
self._files = ret
return ret
@property
def files_status(self):
ret = self._files_status
if ret is None:
ret = [f.split(None, 1) for f in self._log_format("format:", args=("--name-status",)).split(b"\n") if f]
self._files_status = ret
return ret
class GitCommitIter:
__slots__ = (
"_path",
"_git_dir",
"_sha1_range",
"_process",
)
def __init__(self, path, sha1_range):
self._path = path
self._git_dir = os.path.join(path, ".git")
self._sha1_range = sha1_range
self._process = None
def __iter__(self):
cmd = (
"git",
"--git-dir",
self._git_dir,
"log",
self._sha1_range,
"--format=%H",
)
# print(" ".join(cmd))
self._process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
)
return self
def __next__(self):
sha1 = self._process.stdout.readline()[:-1]
if sha1:
return GitCommit(sha1, self._git_dir)
else:
raise StopIteration
class GitRepo:
__slots__ = (
"_path",
"_git_dir",
)
def __init__(self, path):
self._path = path
self._git_dir = os.path.join(path, ".git")
@property
def branch(self):
cmd = (
"git",
"--git-dir",
self._git_dir,
"rev-parse",
"--abbrev-ref",
"HEAD",
)
# print(" ".join(cmd))
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
)
return p.stdout.read()
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
# 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()
+770
View File
@@ -0,0 +1,770 @@
#!/usr/bin/env python3
# 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()
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Created by Robert Wenzlaff (Det. Thorn).
# Oct. 30, 2003
from tkinter import (
Button,
Canvas,
Checkbutton,
END,
Frame,
IntVar,
Label,
RIDGE,
Text,
Tk,
)
color = ("black", "white", "darkgreen", "gray")
class App:
def __init__(self, master):
frame = Frame(master, borderwidth=5)
frame.grid(column=0, row=0, pady=5)
self.state = []
self.states = 256
self.laststate = 2 # 0=Black, 1=White, 2=Transp.
self.size = 16
self.gridsz = 20
for x in range(1024):
self.state.append(2)
self.screen = Canvas(frame, height=320, width=320, bg=color[2])
self.screen.bind("<Button-1>", self.scrnclick1)
self.screen.bind("<Button-3>", self.scrnclick2)
self.screen.bind("<B1-Motion>", self.scrndrag)
for x in range(16):
self.screen.create_line((x * 20, 0, x * 20, 320), fill=color[3])
self.screen.create_line((0, x * 20, 320, x * 20), fill=color[3])
self.screen.grid(row=0, column=0, columnspan=5)
frame2 = Frame(master, borderwidth=5)
frame2.grid(column=0, row=1, pady=5)
self.clear = Button(frame2, text="Clear", command=self.clearit)
self.clear.grid(row=0, column=0, pady=20)
self.doit = Button(frame2, text="Print", command=self.doit)
self.doit.grid(row=0, column=1, pady=20)
#self.doitlab = Label(frame2, text="(Output to stdout)");
#self.doitlab.grid(row=1, column=1);
self.parse = Button(frame2, text="Parse", command=self.parsetext)
self.parse.grid(row=0, column=2, pady=20)
self.large = 0
self.dummy = IntVar()
self.largeb = Checkbutton(frame2, text="Large", var=self.dummy, command=self.changesize)
self.largeb.grid(row=0, column=3, pady=20)
self.prev = Canvas(frame2, height=17, width=17, bg=color[2], relief=RIDGE)
self.prev.grid(row=0, column=4, pady=20, padx=20)
# DataParsers
self.bmlabel = Label(frame2, text="Bitmap Data (paste hex from code)")
self.bmlabel.grid(row=2, column=0, columnspan=5, sticky="W")
self.bmentry = Text(frame2, width=80, height=9, font="Times 8")
self.bmentry.bind("<Leave>", self.bmtextpaste)
self.bmentry.grid(row=3, column=0, columnspan=5, pady=5)
self.msklabel = Label(frame2, text="Mask Data (paste hex from code)")
self.msklabel.grid(row=4, column=0, columnspan=5, sticky="W")
self.mskentry = Text(frame2, width=80, height=9, font="Times 8")
self.mskentry.bind("<Leave>", self.msktextpaste)
self.mskentry.grid(row=5, column=0, columnspan=5, pady=5)
def changesize(self):
self.large = ~self.large
if self.large:
self.size = 32
self.gridsz = 10
self.states = 1024
oldstate = self.state
self.state = []
for n in range(1024):
col = (n // 2) % 16
row = int(n // 64)
self.state.append(oldstate[16 * row + col])
oldstate = []
else:
self.size = 16
self.gridsz = 20
self.states = 256
oldstate = self.state
self.state = []
for n in range(1024):
if not ((n % 2) or ((n // 32) % 2)):
self.state.append(oldstate[n])
for n in range(256, 1024):
self.state.append(2)
oldstate = []
# Insert scaling here
self.updatescrn()
self.prev.config(width=self.size + 1, height=self.size + 1)
for n in range(self.states):
self.updateprev(n)
#self.prev.grid(row=0, column=4, padx=self.gridsz, pady=self.gridsz)
def scrnclick1(self, event):
self.scrnclick(event, 1)
def scrnclick2(self, event):
self.scrnclick(event, -1)
def scrnclick(self, event, direction):
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
return
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
self.state[n] += direction
self.state[n] %= 3
row = n % self.size
col = n // self.size
self.screen.create_rectangle((self.gridsz * row + 1,
self.gridsz * col + 1,
self.gridsz * row + self.gridsz - 1,
self.gridsz * col + self.gridsz - 1),
fill=color[self.state[n]], outline="")
self.laststate = self.state[n]
self.updateprev(n)
def scrndrag(self, event):
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
return
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
row = n % self.size
col = n // self.size
self.screen.create_rectangle((self.gridsz * row + 1,
self.gridsz * col + 1,
self.gridsz * row + self.gridsz - 1,
self.gridsz * col + self.gridsz - 1),
fill=color[self.laststate], outline="")
self.state[n] = self.laststate
self.updateprev(n)
def updateprev(self, n):
x = n % self.size + 1
y = n // self.size + 1
if self.large:
pad = 12
else:
pad = 20
self.prev.create_line(x + 1, y + 1, x + 2, y + 1, fill=color[self.state[n]])
self.prev.grid(row=0, column=4, padx=pad, pady=pad)
def updatescrn(self):
self.screen.create_rectangle(0, 0, 320, 320, fill=color[2])
for x in range(self.size):
self.screen.create_line((x * self.gridsz, 0, x * self.gridsz, 320), fill=color[3])
self.screen.create_line((0, x * self.gridsz, 320, x * self.gridsz), fill=color[3])
for n in range(self.states):
row = n % self.size
col = n // self.size
self.screen.create_rectangle((self.gridsz * row + 1,
self.gridsz * col + 1,
self.gridsz * row + self.gridsz - 1,
self.gridsz * col + self.gridsz - 1),
fill=color[self.state[n]], outline="")
def bmtextpaste(self, event):
string = self.bmentry.get(1.0, END)
self.bmentry.delete(1.0, END)
string = string.replace("\t", "")
self.bmentry.insert(END, string)
def msktextpaste(self, event):
string = self.mskentry.get(1.0, END)
self.mskentry.delete(1.0, END)
string = string.replace("\t", "")
self.mskentry.insert(END, string)
def parsetext(self):
bmstring = self.bmentry.get(1.0, END)
bmstring = bmstring.replace(",", " ")
bmstring = bmstring.split()
mskstring = self.mskentry.get(1.0, END)
mskstring = mskstring.replace(",", " ")
mskstring = mskstring.split()
if len(bmstring) != len(mskstring):
print("Mismatched data. Bitmap and mask must be same size,")
return
elif not (len(bmstring) == 32 or len(bmstring) == 128):
print("Size Error, input must be 32 or 128 hex bytes. ")
return
for n in range(self.states):
self.state[n] = 0
m = 0
for entry in bmstring:
e = int(entry, 16)
for bit in range(8):
self.state[m] = (e & 1)
e = e >> 1
m += 1
m = 0
for entry in mskstring:
e = int(entry, 16)
for bit in range(8):
if not (e & 1):
self.state[m] = 2
e = e >> 1
m += 1
self.updatescrn()
for n in range(self.states):
self.updateprev(n)
def clearit(self):
for n in range(self.states):
self.state[n] = 2
self.updateprev(n)
self.updatescrn()
self.bmentry.delete(0.0, END)
self.mskentry.delete(0.0, END)
def doit(self):
mask = []
bitmap = []
numbytes = self.size * self.size // 8
for i in range(numbytes):
m = 0
b = 0
for j in range(8):
m <<= 1
b <<= 1
if (self.state[(i * 8) + (7 - j)] != 2):
m |= 1
if (self.state[(i * 8) + (7 - j)] == 1):
b |= 1
#print((i * 8) + (7 - j), self.state[(i * 8) + (7 - j)], m)
mask.append(m)
bitmap.append(b)
print("\n\nstatic char bitmap[] = {", end=' ')
for i in range(numbytes):
b1 = bitmap[i]
if not (i % 8):
print("\n\t", end=' ')
print("0x%(b1)02x, " % vars(), end=' ')
print("\n};")
print("\nstatic char mask[] = {", end=' ')
for i in range(numbytes):
b1 = mask[i]
if not (i % 8):
print("\n\t", end=' ')
print("0x%(b1)02x, " % vars(), end=' ')
print("\n};")
################## Main App #######################
root = Tk()
app = App(root)
root.title("Cursor Maker")
root.mainloop()
+78
View File
@@ -0,0 +1,78 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Converts 32x32 XPM images written be the gimp to GL stipples
# takes XPM files as arguments, prints out C style definitions.
import sys
import os
def main():
xpm_ls = [f for f in sys.argv[1:] if f.lower().endswith(".xpm")]
print("Converting: " + " ".join(xpm_ls))
for xpm in xpm_ls:
f = open(xpm, "r")
data = f.read()
f.close()
# all after first {
data = data.split("{", 1)[1]
# all before first }
data = data.rsplit("}", 1)[0]
data = data.replace("\n", "")
data = data.split(",")
w, h, c, dummy = map(int, data[0].strip("\"").split())
if w != 32 or h != 32 or c != 2:
print("Skipping %r, expected 32x32, monochrome, got %s" %
(xpm, data[0]))
continue
# col_1 = data[1][1]
col_2 = data[2][1]
data = [d[1:-1] for d in data[3:]]
bits = []
for d in data:
for i, c in enumerate(d):
bits.append('01'[(c == col_2)])
if len(bits) != 1024:
print("Skipping %r, expected 1024 bits, got %d" %
(xpm, len(bits)))
continue
bits = "".join(bits)
chars = []
for i in range(0, len(bits), 8):
chars.append("0x%.2x" % int(bits[i:i + 8], 2))
fout = sys.stdout
var = os.path.basename(xpm)
var = os.path.splitext(var)[0]
fout.write("GLubyte stipple_%s[128] {\n\t" % var)
for i, c in enumerate(chars):
if i != 127:
fout.write("%s, " % c)
else:
fout.write("%s" % c)
if not ((i + 1) % 8):
fout.write("\n\t")
fout.write("};\n")
if __name__ == "__main__":
main()
+143
View File
@@ -0,0 +1,143 @@
# SPDX-License-Identifier: Apache-2.0
'''
Created compact byte arrays which can be decoded into 2D shapes.
(See 'GPU_batch_from_poly_2d_encoded').
- Objects must use the prefix "shape_"
- Meshes and Curves are supported as input.
- C and Python output is written to "output/"
The format is simple: a series of (X, Y) locations one byte each.
Repeating the same value terminates the polygon, moving onto the next.
Example Use::
blender.bin -b --factory-startup my_shapes.blend --python make_shape_2d_from_blend.py
'''
import bpy
import os
USE_C_STYLE = True
USE_PY_STYLE = True
WRAP_LIMIT = 79
TAB_WIDTH = 4
SUBDIR = "output"
def float_to_ubyte(f):
return max(0, min(255, int(round(f * 255.0))))
def curve_to_loops(ob):
import bmesh
cu = ob.data
me = ob.to_mesh()
bm = bmesh.new()
bm.from_mesh(me)
me = ob.to_mesh_clear()
bmesh.ops.beautify_fill(bm, faces=bm.faces, edges=bm.edges)
edges = bm.edges[:]
edges.sort(key=lambda e: e.calc_length(), reverse=True)
for e in edges:
if e.is_manifold:
f_a, f_b = [f for f in e.link_faces]
bmesh.utils.face_join((f_a, f_b), False)
edges = bm.edges[:]
for e in edges:
if e.is_wire:
bm.edges.remove(e)
bm.normal_update()
data_all = []
for f in bm.faces:
points = []
# Ensure all faces are pointing the correct direction
# Note, we may want to use polygon sign for a second color
# (via the material index).
if f.normal.z > 0.0:
loops = f.loops
else:
loops = reversed(f.loops)
for l in loops:
points.append(
tuple(float_to_ubyte(axis) for axis in l.vert.co.xy)
)
data_all.append((points, f.material_index))
bm.free()
return data_all
def write_c(ob):
cu = ob.data
name = ob.name
with open(os.path.join(SUBDIR, name + ".c"), 'w') as fh:
fw = fh.write
fw(f"/* {name} */\n")
fw(f"const uchar {name}[] = {{")
line_len = WRAP_LIMIT
line_is_first = True
array_len = 0
data_all = curve_to_loops(ob)
for (points, material_index) in data_all:
# TODO, material_index
for p in points + [points[-1]]:
line_len += 12
if line_len >= WRAP_LIMIT:
fw("\n\t")
line_len = TAB_WIDTH
line_is_first = True
if not line_is_first:
fw(" ")
fw(", ".join([f"0x{axis:02x}" for axis in p]) + ",")
line_is_first = False
array_len += (len(points) + 1) * 2
fw("\n};\n")
# fw(f"const int data_len = {array_len}\n")
def write_py(ob):
cu = ob.data
name = ob.name
with open(os.path.join(SUBDIR, name + ".py"), 'w') as fh:
fw = fh.write
fw(f"# {name}\n")
fw("data = (")
line_len = WRAP_LIMIT
fw = fh.write
data_all = curve_to_loops(ob)
for (points, material_index) in data_all:
# TODO, material_index
for p in points + [points[-1]]:
line_len += 8
if line_len >= WRAP_LIMIT:
if p is not points[0]:
fw("'")
fw("\n b'")
line_len = 6
fw("".join([f"\\x{axis:02x}" for axis in p]))
fw("'\n)\n")
def main():
os.makedirs(SUBDIR, exist_ok=True)
for ob in bpy.data.objects:
if ob.type not in {'MESH', 'CURVE'}:
continue
if not ob.name.startswith('shape_'):
continue
if USE_C_STYLE:
write_c(ob)
if USE_PY_STYLE:
write_py(ob)
if __name__ == "__main__":
main()