From b2c26f7cdd4b268e80f98cae7f444956559436ec Mon Sep 17 00:00:00 2001 From: Zach White Date: Tue, 1 Dec 2020 16:04:22 -0800 Subject: get qmk generate-api into a good state --- lib/python/qmk/c_parse.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'lib/python/qmk/c_parse.py') diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index e41e271a43..67e196f0ea 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -1,12 +1,27 @@ """Functions for working with config.h files. """ from pathlib import Path +import re from milc import cli from qmk.comment_remover import comment_remover default_key_entry = {'x': -1, 'y': 0, 'w': 1} +single_comment_regex = re.compile(r' */[/*].*$') +multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE) + + +def strip_line_comment(string): + """Removes comments from a single line string. + """ + return single_comment_regex.sub('', string) + + +def strip_multiline_comment(string): + """Removes comments from a single line string. + """ + return multi_comment_regex.sub('', string) def c_source_files(dir_names): @@ -53,7 +68,8 @@ def find_layouts(file): parsed_layout = [_default_key(key) for key in layout.split(',')] for key in parsed_layout: - key['matrix'] = matrix_locations.get(key['label']) + if key['label'] in matrix_locations: + key['matrix'] = matrix_locations[key['label']] parsed_layouts[macro_name] = { 'key_count': len(parsed_layout), @@ -88,12 +104,10 @@ def parse_config_h_file(config_h_file, config_h=None): if config_h_file.exists(): config_h_text = config_h_file.read_text() config_h_text = config_h_text.replace('\\\n', '') + config_h_text = strip_multiline_comment(config_h_text) for linenum, line in enumerate(config_h_text.split('\n')): - line = line.strip() - - if '//' in line: - line = line[:line.index('//')].strip() + line = strip_line_comment(line).strip() if not line: continue @@ -156,6 +170,6 @@ def _parse_matrix_locations(matrix, file, macro_name): row = row.replace('{', '').replace('}', '') for col_num, identifier in enumerate(row.split(',')): if identifier != 'KC_NO': - matrix_locations[identifier] = (row_num, col_num) + matrix_locations[identifier] = [row_num, col_num] return matrix_locations -- cgit v1.2.3 From 30331b383f9ef4620e47aa07e4f9af7fae9d30b3 Mon Sep 17 00:00:00 2001 From: Zach White Date: Fri, 8 Jan 2021 00:00:15 -0800 Subject: fix bugs triggered by certain boards --- data/schemas/keyboard.jsonschema | 34 ++++++++++++++++++++++++++++---- lib/python/qmk/c_parse.py | 4 ++-- lib/python/qmk/cli/generate/config_h.py | 6 +++--- lib/python/qmk/info.py | 35 ++++++++++++++++++++++----------- 4 files changed, 59 insertions(+), 20 deletions(-) (limited to 'lib/python/qmk/c_parse.py') diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 9355ee49bd..e13771e92a 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -166,6 +166,10 @@ "type": "string", "pattern": "^[A-K]\\d{1,2}$" }, + { + "type": "number", + "multipleOf": 1 + }, { "type": "null" } @@ -176,15 +180,37 @@ "cols": { "type": "array", "items": { - "type": "string", - "pattern": "^[A-K]\\d{1,2}$" + "oneOf": [ + { + "type": "string", + "pattern": "^[A-K]\\d{1,2}$" + }, + { + "type": "number", + "multipleOf": 1 + }, + { + "type": "null" + } + ] } }, "rows": { "type": "array", "items": { - "type": "string", - "pattern": "^[A-K]\\d{1,2}$" + "oneOf": [ + { + "type": "string", + "pattern": "^[A-K]\\d{1,2}$" + }, + { + "type": "number", + "multipleOf": 1 + }, + { + "type": "null" + } + ] } } } diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 67e196f0ea..0338484ec7 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -9,7 +9,7 @@ from qmk.comment_remover import comment_remover default_key_entry = {'x': -1, 'y': 0, 'w': 1} single_comment_regex = re.compile(r' */[/*].*$') -multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE) +multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE) def strip_line_comment(string): @@ -103,7 +103,7 @@ def parse_config_h_file(config_h_file, config_h=None): if config_h_file.exists(): config_h_text = config_h_file.read_text() - config_h_text = config_h_text.replace('\\\n', '') + config_h_text = config_h_text.replace('\\\n', '') # Why are you here? config_h_text = strip_multiline_comment(config_h_text) for linenum, line in enumerate(config_h_text.split('\n')): diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 15d4fbf2dd..1de84de7a9 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -64,7 +64,7 @@ def direct_pins(direct_pins): rows = [] for row in direct_pins: - cols = ','.join([col or 'NO_PIN' for col in row]) + cols = ','.join(map(str, [col or 'NO_PIN' for col in row])) rows.append('{' + cols + '}') col_count = len(direct_pins[0]) @@ -88,7 +88,7 @@ def direct_pins(direct_pins): def col_pins(col_pins): """Return the config.h lines that set the column pins. """ - cols = ','.join(col_pins) + cols = ','.join(map(str, [pin or 'NO_PIN' for pin in col_pins])) col_num = len(col_pins) return """ @@ -105,7 +105,7 @@ def col_pins(col_pins): def row_pins(row_pins): """Return the config.h lines that set the row pins. """ - rows = ','.join(row_pins) + rows = ','.join(map(str, [pin or 'NO_PIN' for pin in row_pins])) row_num = len(row_pins) return """ diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index efd339115b..28c281a4bc 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -315,11 +315,10 @@ def _extract_rgblight(info_data, config_c): cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e) for json_key, config_key in rgblight_toggles.items(): - if config_key in config_c: - if json_key in rgblight: - _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key) + if config_key in config_c and json_key in rgblight: + _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key) - rgblight[json_key] = config_c[config_key] + rgblight[json_key] = config_key in config_c for json_key, config_key in rgblight_animations.items(): if config_key in config_c: @@ -337,16 +336,30 @@ def _extract_rgblight(info_data, config_c): return info_data -def _extract_pins(pins): - """Returns a list of pins from a comma separated string of pins. +def _pin_name(pin): + """Returns the proper representation for a pin. """ - pins = [pin.strip() for pin in pins.split(',') if pin] + pin = pin.strip() + + if not pin: + return None + + elif pin.isdigit(): + return int(pin) - for pin in pins: - if pin[0] not in 'ABCDEFGHIJK' or not pin[1].isdigit(): - raise ValueError(f'Invalid pin: {pin}') + elif pin == 'NO_PIN': + return None - return pins + elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): + return pin + + raise ValueError(f'Invalid pin: {pin}') + + +def _extract_pins(pins): + """Returns a list of pins from a comma separated string of pins. + """ + return [_pin_name(pin) for pin in pins.split(',')] def _extract_direct_matrix(info_data, direct_pins): -- cgit v1.2.3 From 58fcdf8c07e5c1363b6b3eaf23883853dfd12f53 Mon Sep 17 00:00:00 2001 From: Zach White Date: Fri, 8 Jan 2021 00:21:51 -0800 Subject: remove extraneous comment --- lib/python/qmk/c_parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/python/qmk/c_parse.py') diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 0338484ec7..ade3e38059 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -103,7 +103,7 @@ def parse_config_h_file(config_h_file, config_h=None): if config_h_file.exists(): config_h_text = config_h_file.read_text() - config_h_text = config_h_text.replace('\\\n', '') # Why are you here? + config_h_text = config_h_text.replace('\\\n', '') config_h_text = strip_multiline_comment(config_h_text) for linenum, line in enumerate(config_h_text.split('\n')): -- cgit v1.2.3 From 23ef327e118307d276677d30e3fda064ace6713b Mon Sep 17 00:00:00 2001 From: Zach White Date: Wed, 24 Feb 2021 10:35:08 -0800 Subject: make LAYOUT parsing more robust --- lib/python/qmk/c_parse.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/python/qmk/c_parse.py') diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index ade3e38059..89dd278b7e 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -67,8 +67,10 @@ def find_layouts(file): layout = layout.strip() parsed_layout = [_default_key(key) for key in layout.split(',')] - for key in parsed_layout: - if key['label'] in matrix_locations: + for i, key in enumerate(parsed_layout): + if 'label' not in key: + cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i) + elif key['label'] in matrix_locations: key['matrix'] = matrix_locations[key['label']] parsed_layouts[macro_name] = { -- cgit v1.2.3 From 1581ea48dcd48d0d3f42cc09b388c468aedec45d Mon Sep 17 00:00:00 2001 From: Zach White Date: Sat, 27 Feb 2021 12:00:50 -0800 Subject: Fix develop (#12039) Fixes file encoding errors on Windows, and layouts not correctly merging into info.json. * force utf8 encoding * correctly merge layouts and layout aliases * show what aliases point to --- data/schemas/keyboard.jsonschema | 12 ++++++-- lib/python/qmk/c_parse.py | 11 ++------ lib/python/qmk/cli/chibios/confmigrate.py | 8 +++--- lib/python/qmk/cli/generate/layouts.py | 4 +++ lib/python/qmk/cli/info.py | 7 +++-- lib/python/qmk/cli/kle2json.py | 2 +- lib/python/qmk/info.py | 44 ++++++++++++++++++++++------- lib/python/qmk/keymap.py | 6 ++-- lib/python/qmk/os_helpers/linux/__init__.py | 2 +- lib/python/qmk/tests/test_cli_commands.py | 2 +- 10 files changed, 66 insertions(+), 32 deletions(-) (limited to 'lib/python/qmk/c_parse.py') diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 967b5f9904..f5fb611bd2 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -85,8 +85,16 @@ "layout_aliases": { "type": "object", "additionalProperties": { - "type": "string", - "pattern": "^LAYOUT_[0-9a-z_]*$" + "oneOf": [ + { + "type": "string", + "enum": ["LAYOUT"] + }, + { + "type": "string", + "pattern": "^LAYOUT_[0-9a-z_]*$" + } + ] } }, "layouts": { diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 89dd278b7e..d4f39c8839 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -46,7 +46,7 @@ def find_layouts(file): parsed_layouts = {} # Search the file for LAYOUT macros and aliases - file_contents = file.read_text() + file_contents = file.read_text(encoding='utf-8') file_contents = comment_remover(file_contents) file_contents = file_contents.replace('\\\n', '') @@ -87,12 +87,7 @@ def find_layouts(file): except ValueError: continue - # Populate our aliases - for alias, text in aliases.items(): - if text in parsed_layouts and 'KEYMAP' not in alias: - parsed_layouts[alias] = parsed_layouts[text] - - return parsed_layouts + return parsed_layouts, aliases def parse_config_h_file(config_h_file, config_h=None): @@ -104,7 +99,7 @@ def parse_config_h_file(config_h_file, config_h=None): config_h_file = Path(config_h_file) if config_h_file.exists(): - config_h_text = config_h_file.read_text() + config_h_text = config_h_file.read_text(encoding='utf-8') config_h_text = config_h_text.replace('\\\n', '') config_h_text = strip_multiline_comment(config_h_text) diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py index 3e348b2b07..89995931a4 100644 --- a/lib/python/qmk/cli/chibios/confmigrate.py +++ b/lib/python/qmk/cli/chibios/confmigrate.py @@ -40,7 +40,7 @@ file_header = """\ def collect_defines(filepath): - with open(filepath, 'r') as f: + with open(filepath, 'r', encoding='utf-8') as f: content = f.read() define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE) value_search = re.compile(r'^#\s*define\s+(?P[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P.*)', re.DOTALL) @@ -146,17 +146,17 @@ def chibios_confmigrate(cli): if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force): migrate_chconf_h(to_override, outfile=sys.stdout) if cli.args.overwrite: - with open(cli.args.input, "w") as out_file: + with open(cli.args.input, "w", encoding='utf-8') as out_file: migrate_chconf_h(to_override, outfile=out_file) elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force): migrate_halconf_h(to_override, outfile=sys.stdout) if cli.args.overwrite: - with open(cli.args.input, "w") as out_file: + with open(cli.args.input, "w", encoding='utf-8') as out_file: migrate_halconf_h(to_override, outfile=out_file) elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force): migrate_mcuconf_h(to_override, outfile=sys.stdout) if cli.args.overwrite: - with open(cli.args.input, "w") as out_file: + with open(cli.args.input, "w", encoding='utf-8') as out_file: migrate_mcuconf_h(to_override, outfile=out_file) diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py index b7baae0651..15b289522e 100755 --- a/lib/python/qmk/cli/generate/layouts.py +++ b/lib/python/qmk/cli/generate/layouts.py @@ -82,6 +82,10 @@ def generate_layouts(cli): layouts_h_lines.append(rows) layouts_h_lines.append('}') + for alias, target in kb_info_json.get('layout_aliases', {}).items(): + layouts_h_lines.append('') + layouts_h_lines.append('#define %s %s' % (alias, target)) + # Show the results layouts_h = '\n'.join(layouts_h_lines) + '\n' diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index 87d7253d4b..a7ce8abf03 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -29,7 +29,7 @@ def show_keymap(kb_info_json, title_caps=True): else: cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap) - keymap_data = json.load(keymap_path.open()) + keymap_data = json.load(keymap_path.open(encoding='utf-8')) layout_name = keymap_data['layout'] for layer_num, layer in enumerate(keymap_data['layers']): @@ -57,7 +57,7 @@ def show_matrix(kb_info_json, title_caps=True): # Build our label list labels = [] for key in layout['layout']: - if key['matrix']: + if 'matrix' in key: row = ROW_LETTERS[key['matrix'][0]] col = COL_LETTERS[key['matrix'][1]] @@ -91,6 +91,9 @@ def print_friendly_output(kb_info_json): cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height'])) cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown')) cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown')) + if 'layout_aliases' in kb_info_json: + aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()] + cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),)) if cli.config.info.layouts: show_layouts(kb_info_json, True) diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 66d504bfc2..3bb7443582 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -27,7 +27,7 @@ def kle2json(cli): cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path) return False out_path = file_path.parent - raw_code = file_path.open().read() + raw_code = file_path.read_text(encoding='utf-8') # Check if info.json exists, allow overwrite with force if Path(out_path, "info.json").exists() and not cli.args.force: cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 2accaba9e4..cf5dc6640b 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -45,7 +45,12 @@ def info_json(keyboard): info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} # Populate layout data - for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items(): + layouts, aliases = _find_all_layouts(info_data, keyboard) + + if aliases: + info_data['layout_aliases'] = aliases + + for layout_name, layout_json in layouts.items(): if not layout_name.startswith('LAYOUT_kc'): layout_json['c_macro'] = True info_data['layouts'][layout_name] = layout_json @@ -92,7 +97,7 @@ def _json_load(json_file): Note: file must be a Path object. """ try: - return hjson.load(json_file.open()) + return hjson.load(json_file.open(encoding='utf-8')) except json.decoder.JSONDecodeError as e: cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e) @@ -415,21 +420,28 @@ def _merge_layouts(info_data, new_info_data): def _search_keyboard_h(path): current_path = Path('keyboards/') + aliases = {} layouts = {} + for directory in path.parts: current_path = current_path / directory keyboard_h = '%s.h' % (directory,) keyboard_h_path = current_path / keyboard_h if keyboard_h_path.exists(): - layouts.update(find_layouts(keyboard_h_path)) + new_layouts, new_aliases = find_layouts(keyboard_h_path) + layouts.update(new_layouts) + + for alias, alias_text in new_aliases.items(): + if alias_text in layouts: + aliases[alias] = alias_text - return layouts + return layouts, aliases def _find_all_layouts(info_data, keyboard): """Looks for layout macros associated with this keyboard. """ - layouts = _search_keyboard_h(Path(keyboard)) + layouts, aliases = _search_keyboard_h(Path(keyboard)) if not layouts: # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above. @@ -437,11 +449,15 @@ def _find_all_layouts(info_data, keyboard): for file in glob('keyboards/%s/*.h' % keyboard): if file.endswith('.h'): - these_layouts = find_layouts(file) + these_layouts, these_aliases = find_layouts(file) + if these_layouts: layouts.update(these_layouts) - return layouts + if these_aliases: + aliases.update(these_aliases) + + return layouts, aliases def _log_error(info_data, message): @@ -540,11 +556,19 @@ def merge_info_jsons(keyboard, info_data): cli.log.error('\t%s: %s', json_path, e.message) continue - # Mark the layouts as coming from json - for layout in new_info_data.get('layouts', {}).values(): - layout['c_macro'] = False + # Merge layout data in + for layout_name, layout in new_info_data.get('layouts', {}).items(): + if layout_name in info_data['layouts']: + for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']): + existing_key.update(new_key) + else: + layout['c_macro'] = False + info_data['layouts'][layout_name] = layout # Update info_data with the new data + if 'layouts' in new_info_data: + del (new_info_data['layouts']) + deep_update(info_data, new_info_data) return info_data diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 266532f503..d8495c38bc 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -42,7 +42,7 @@ def template_json(keyboard): template_file = Path('keyboards/%s/templates/keymap.json' % keyboard) template = {'keyboard': keyboard} if template_file.exists(): - template.update(json.loads(template_file.read_text())) + template.update(json.load(template_file.open(encoding='utf-8'))) return template @@ -58,7 +58,7 @@ def template_c(keyboard): """ template_file = Path('keyboards/%s/templates/keymap.c' % keyboard) if template_file.exists(): - template = template_file.read_text() + template = template_file.read_text(encoding='utf-8') else: template = DEFAULT_KEYMAP_C @@ -469,7 +469,7 @@ def parse_keymap_c(keymap_file, use_cpp=True): if use_cpp: keymap_file = _c_preprocess(keymap_file) else: - keymap_file = keymap_file.read_text() + keymap_file = keymap_file.read_text(encoding='utf-8') keymap = dict() keymap['layers'] = _get_layers(keymap_file) diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py index a04ac4f8a9..9e73964e47 100644 --- a/lib/python/qmk/os_helpers/linux/__init__.py +++ b/lib/python/qmk/os_helpers/linux/__init__.py @@ -95,7 +95,7 @@ def check_udev_rules(): # Collect all rules from the config files for rule_file in udev_rules: - for line in rule_file.read_text().split('\n'): + for line in rule_file.read_text(encoding='utf-8').split('\n'): line = line.strip() if not line.startswith("#") and len(line): current_rules.add(line) diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index 3efeddb85e..82c42a20e8 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -16,7 +16,7 @@ def check_subcommand(command, *args): def check_subcommand_stdin(file_to_read, command, *args): """Pipe content of a file to a command and return output. """ - with open(file_to_read) as my_file: + with open(file_to_read, encoding='utf-8') as my_file: cmd = ['bin/qmk', command, *args] result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True) return result -- cgit v1.2.3