git-subtree-dir: external/capstone git-subtree-split: e46f64fadb351e9ecd05264fab26f2772feb0994
1009 lines
37 KiB
Python
Executable File
1009 lines
37 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright © 2022 Rot127 <unisono@quyllur.org>
|
|
# SPDX-License-Identifier: BSD-3
|
|
|
|
import argparse
|
|
import difflib as dl
|
|
import json
|
|
import logging as log
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from enum import StrEnum
|
|
from pathlib import Path
|
|
from shutil import copyfile
|
|
|
|
from tree_sitter import Language, Node, Parser, Tree, Point
|
|
|
|
from autosync.Targets import ARCH_LLVM_NAMING
|
|
from autosync.cpptranslator.Configurator import Configurator
|
|
from autosync.Helper import (
|
|
bold,
|
|
colored,
|
|
convert_loglevel,
|
|
find_id_by_type,
|
|
get_path,
|
|
get_sha256,
|
|
print_prominent_info,
|
|
print_prominent_warning,
|
|
run_clang_format,
|
|
separator_line_1,
|
|
separator_line_2,
|
|
)
|
|
|
|
|
|
class PatchCoord:
|
|
"""Holds the coordinate information of tree-sitter nodes."""
|
|
|
|
start_byte: int
|
|
end_byte: int
|
|
start_point: Point
|
|
end_point: Point
|
|
|
|
def __init__(
|
|
self,
|
|
start_byte: int,
|
|
end_byte: int,
|
|
start_point: Point,
|
|
end_point: Point,
|
|
inserted: bool = False,
|
|
):
|
|
self.start_byte = start_byte
|
|
self.end_byte = end_byte
|
|
self.start_point = start_point
|
|
self.end_point = end_point
|
|
self.inserted = inserted # True, if the PatchCoordinates point to a position not occupied by a node.
|
|
|
|
def __lt__(self, other):
|
|
return self.end_byte < other.start_byte
|
|
|
|
def __str__(self) -> str:
|
|
return f"s: {self.start_byte} e: {self.end_byte}"
|
|
|
|
@staticmethod
|
|
def get_coordinates_from_node(node: Node):
|
|
return PatchCoord(
|
|
node.start_byte, node.end_byte, node.start_point, node.end_point
|
|
)
|
|
|
|
|
|
class ApplyType(StrEnum):
|
|
OLD = "OLD" # Apply version from old file
|
|
NEW = "NEW" # Apply version from new file (leave unchanged)
|
|
SAVED = "SAVED" # Use saved resolution
|
|
EDIT = "EDIT" # Edit patch and apply
|
|
SHOW_EDIT = "SHOW_EDIT" # Show the saved edited text.
|
|
OLD_ALL = "OLD_ALL" # Apply all versions from old file.
|
|
PREVIOUS = "PREVIOUS" # Ignore diff and go to previous
|
|
|
|
|
|
class Patch:
|
|
node_id: str
|
|
coord: PatchCoord
|
|
apply: ApplyType
|
|
old: bytes
|
|
new: bytes
|
|
edit: bytes
|
|
old_hash: str
|
|
new_hash: str
|
|
|
|
def __init__(
|
|
self,
|
|
node_id: str,
|
|
old: bytes,
|
|
new: bytes,
|
|
coord: PatchCoord,
|
|
apply: ApplyType,
|
|
edit: bytes = None,
|
|
) -> None:
|
|
if apply == ApplyType.SAVED:
|
|
raise NotImplementedError("Not yet implemented.")
|
|
self.node_id = node_id
|
|
self.apply = apply
|
|
self.coord = coord
|
|
self.old = old
|
|
self.new = new
|
|
self.edit = edit
|
|
self.old_hash = ""
|
|
self.new_hash = ""
|
|
if self.old:
|
|
self.old_hash = get_sha256(self.old)
|
|
if self.new:
|
|
self.new_hash = get_sha256(self.new)
|
|
|
|
def get_persist_info(self) -> dict:
|
|
"""Returns a dictionary with the relevant information to back up this patch."""
|
|
backup = dict()
|
|
backup[self.node_id] = dict()
|
|
backup[self.node_id]["apply_type"] = str(self.apply)
|
|
backup[self.node_id]["old_hash"] = self.old_hash
|
|
backup[self.node_id]["new_hash"] = self.new_hash
|
|
backup[self.node_id]["edit"] = self.edit.decode("utf8") if self.edit else ""
|
|
return backup
|
|
|
|
def __lt__(self, other):
|
|
try:
|
|
return self.coord < other.coord
|
|
except IndexError:
|
|
raise IndexError(f"Nodes overlap: {self} - {other}")
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.node_id} @ {self.coord}"
|
|
|
|
|
|
class Differ:
|
|
"""
|
|
Diffs the newly translated C++ files against the old version.
|
|
|
|
The general diffing works like this:
|
|
|
|
The old and the new file get parsed with tree sitter into an AST.
|
|
Then, we extract all nodes of a specific type out of this AST.
|
|
Which nodes specifically is defined in "arch_config.json::General::nodes_to_diff".
|
|
|
|
These nodes (old and new separately) are then sorted descending by their coordinates.
|
|
Meaning, nodes at the end of the files come first.
|
|
The identifiers of those nodes are saved in a single list.
|
|
Now we iterate over this list of identifiers and make decisions:
|
|
|
|
The node id is present in:
|
|
old and new file => Text matches?
|
|
yes => Continue
|
|
no => Patch node (see below)
|
|
only in one file => Insert node at a suitable point (documented below in code).
|
|
A node is never removed from a file. This is the responsibility of the user when doing clean up.
|
|
|
|
Now we have the patch. We have a persistence file which previous decisions, on which patch to choose.
|
|
We take the node text of the old and new node (or only from a single one) and compare them to our previous decision.
|
|
If the text of the nodes didn't change since the last run, we auto-apply the patch.
|
|
Otherwise, the user decides:
|
|
- Choose the old node text
|
|
- Choose the new node text
|
|
- Open the editor to edit the patch and apply it.
|
|
- Use the stored previous decision.
|
|
- Select always the old nodes from now on.
|
|
- Go back and decide on the node before.
|
|
|
|
Each decision is saved to the persistence file for later.
|
|
"""
|
|
|
|
ts_cpp_lang: Language = None
|
|
parser: Parser = None
|
|
translated_files: [Path]
|
|
patched_file_paths: dict[str:Path] = dict()
|
|
old_files: [Path]
|
|
conf_arch: dict
|
|
conf_general: dict
|
|
tree: Tree = None
|
|
persistence_filepath: Path
|
|
saved_patches: dict = (
|
|
None # Saved patches, indexed by old file names and node identifiers.
|
|
)
|
|
patches: list[Patch]
|
|
|
|
cur_old_node: Node | None = None
|
|
cur_new_node: Node | None = None
|
|
cur_nid: str = ""
|
|
|
|
def __init__(
|
|
self,
|
|
configurator: Configurator,
|
|
no_auto_apply: bool,
|
|
testing: bool = False,
|
|
check_saved: bool = False,
|
|
edit_old_file: bool = True,
|
|
):
|
|
self.configurator = configurator
|
|
self.no_auto_apply = no_auto_apply
|
|
self.arch = self.configurator.get_arch()
|
|
self.conf_arch = self.configurator.get_arch_config()
|
|
self.conf_general = self.configurator.get_general_config()
|
|
self.ts_cpp_lang = self.configurator.get_cpp_lang()
|
|
self.parser = self.configurator.get_parser()
|
|
self.differ = dl.Differ()
|
|
self.testing = testing
|
|
self.check_saved = check_saved
|
|
self.edit_old_file = (
|
|
edit_old_file # If true, the changes are written to the old file.
|
|
)
|
|
self.user_choices = list()
|
|
|
|
if self.testing:
|
|
self.diff_out_dir = get_path("{DIFFER_TEST_OUTPUT_DIR}")
|
|
t_out_dir: Path = get_path("{DIFFER_TEST_NEW_SRC_DIR}")
|
|
self.translated_files = [
|
|
t_out_dir.joinpath(sp["out"])
|
|
for sp in self.conf_arch["files_to_translate"]
|
|
]
|
|
self.old_files = [
|
|
get_path("{DIFFER_TEST_OLD_SRC_DIR}").joinpath(sp["out"])
|
|
for sp in self.conf_arch["files_to_translate"]
|
|
]
|
|
self.load_persistence_file()
|
|
else:
|
|
self.diff_out_dir = get_path("{CPP_TRANSLATOR_DIFF_OUT_DIR}")
|
|
t_out_dir: Path = get_path("{CPP_TRANSLATOR_TRANSLATION_OUT_DIR}")
|
|
self.translated_files = [
|
|
t_out_dir.joinpath(sp["out"])
|
|
for sp in self.conf_arch["files_to_translate"]
|
|
]
|
|
cs_arch_src: Path = get_path("{CS_ARCH_MODULE_DIR}")
|
|
cs_arch_src = cs_arch_src.joinpath(
|
|
self.arch if self.arch != "PPC" else "PowerPC"
|
|
)
|
|
self.old_files = [
|
|
cs_arch_src.joinpath(f"{cs_arch_src}/" + sp["out"])
|
|
for sp in self.conf_arch["files_to_translate"]
|
|
]
|
|
self.load_persistence_file()
|
|
if check_saved:
|
|
self.check_saved_patches()
|
|
print("Save file is up-to-date.")
|
|
|
|
def load_persistence_file(self) -> None:
|
|
if self.testing:
|
|
self.persistence_filepath = get_path("{DIFFER_TEST_PERSISTENCE_FILE}")
|
|
else:
|
|
self.persistence_filepath = get_path("{DIFFER_PERSISTENCE_FILE}")
|
|
if not self.persistence_filepath.exists():
|
|
self.saved_patches = dict()
|
|
return
|
|
|
|
with open(self.persistence_filepath, "rb") as f:
|
|
try:
|
|
self.saved_patches = json.load(f)
|
|
except json.decoder.JSONDecodeError as e:
|
|
log.fatal(
|
|
f"Persistence file {bold(self.persistence_filepath.name)} corrupt."
|
|
)
|
|
log.fatal("Delete it or fix it by hand.")
|
|
log.fatal(f"JSON Exception: {e}")
|
|
exit(1)
|
|
|
|
def save_to_persistence_file(self) -> None:
|
|
if self.testing:
|
|
print("Testing: Skip saving to persistent file...")
|
|
return
|
|
print("\nSave choices...\n")
|
|
with open(self.persistence_filepath, "w") as f:
|
|
json.dump(self.saved_patches, f, indent=2)
|
|
|
|
def persist_patch(self, filename: Path, patch: Patch) -> None:
|
|
"""
|
|
:param filename: The filename this patch is saved for.
|
|
:param patch: The patch to apply.
|
|
"""
|
|
if filename.name not in self.saved_patches:
|
|
self.saved_patches[filename.name] = dict()
|
|
log.debug(f"Save: {patch.get_persist_info()}")
|
|
self.saved_patches[filename.name].update(patch.get_persist_info())
|
|
|
|
def copy_files(self) -> None:
|
|
"""
|
|
Copy translated files to diff directory for editing.
|
|
"""
|
|
log.info("Copy files for editing")
|
|
diff_dir: Path = self.diff_out_dir
|
|
to_copy = self.old_files if self.edit_old_file else self.translated_files
|
|
for f in to_copy:
|
|
if not f.exists():
|
|
log.fatal(
|
|
f"Old version of '{f}' doesn't exist. But the Differ needs an old file for each translated one."
|
|
)
|
|
exit(1)
|
|
dest = diff_dir.joinpath(f.name)
|
|
copyfile(f, dest)
|
|
self.patched_file_paths[f.name] = dest
|
|
|
|
def get_diff_intro_msg(
|
|
self,
|
|
old_filename: Path,
|
|
new_filename: Path,
|
|
current: int,
|
|
total: int,
|
|
num_diffs: int,
|
|
) -> str:
|
|
color_new = self.conf_general["diff_color_new"]
|
|
color_old = self.conf_general["diff_color_old"]
|
|
written_to = (
|
|
bold("OLD FILE", color_old)
|
|
if self.edit_old_file
|
|
else bold("NEW FILE", color_new)
|
|
)
|
|
return (
|
|
f"{bold(f'Diffing files - {current}/{total}')} \n\n"
|
|
+ f"{bold('NEW FILE: ', color_new)} {str(new_filename)}\n"
|
|
+ f"{bold('OLD FILE: ', color_old)} {str(old_filename)}\n\n"
|
|
+ f"{bold('Diffs to process: ')} {num_diffs}\n\n"
|
|
+ f"{bold('Changes are written to: ')} {written_to}\n"
|
|
)
|
|
|
|
def get_diff_node_id(self, node: Node) -> bytes:
|
|
"""
|
|
Searches in the nodes children for the identifier node and returns its text.
|
|
"""
|
|
id_types = [""]
|
|
for n in self.conf_general["nodes_to_diff"]:
|
|
if n["node_type"] == node.type:
|
|
id_types = n["identifier_node_type"]
|
|
if not id_types:
|
|
log.fatal(
|
|
f"Diffing: Node of type {node.type} has not identifier type specified."
|
|
)
|
|
exit(1)
|
|
identifier = ""
|
|
for id_type in id_types:
|
|
identifier = find_id_by_type(node, id_type.split("/"), False)
|
|
if identifier:
|
|
break
|
|
if not identifier:
|
|
log.fatal(f'Diffing: Cannot find node type "{id_types}" in named-children.')
|
|
exit(1)
|
|
return identifier
|
|
|
|
def parse_file(self, file: Path) -> dict[str:Node]:
|
|
"""
|
|
Parse a files and return all nodes which should be diffed.
|
|
Nodes are indexed by a unique identifier.
|
|
"""
|
|
with open(file) as f:
|
|
content = bytes(f.read(), "utf8")
|
|
|
|
tree: Tree = self.parser.parse(content)
|
|
|
|
node_types_to_diff = [
|
|
n["node_type"] for n in self.conf_general["nodes_to_diff"]
|
|
]
|
|
content = None
|
|
if file.suffix == ".h":
|
|
# Header file. Get the content in between the include guard
|
|
for n in tree.root_node.named_children:
|
|
if n.type == "preproc_ifdef":
|
|
content = n
|
|
break
|
|
if not content:
|
|
content = tree.root_node
|
|
duplicates = list()
|
|
nodes_to_diff = dict()
|
|
node: Node
|
|
# Get diff candidates and add them to the dict.
|
|
for node in content.named_children:
|
|
if node.type not in node_types_to_diff:
|
|
continue
|
|
identifier = self.get_diff_node_id(node).decode("utf8")
|
|
if identifier in nodes_to_diff.keys() or identifier in duplicates:
|
|
# This happens if the chosen identifier is not unique.
|
|
log.info(f"Duplicate {bold(identifier)}: Nodes will not be diffed!")
|
|
if identifier in nodes_to_diff.keys():
|
|
nodes_to_diff.pop(identifier)
|
|
duplicates.append(identifier)
|
|
continue
|
|
log.debug(f"Add node to diff: {identifier}")
|
|
nodes_to_diff[identifier] = node
|
|
return nodes_to_diff
|
|
|
|
def print_diff(self, diff_lines: list[str], node_id: str, current: int, total: int):
|
|
new_color = self.conf_general["diff_color_new"]
|
|
old_color = self.conf_general["diff_color_old"]
|
|
print(separator_line_2())
|
|
print(f"{bold('Patch:')} {current}/{total}\n")
|
|
print(f"{bold('Node:')} {node_id}")
|
|
print(f"{bold('Color:')} {colored('NEW FILE - (Just translated)', new_color)}")
|
|
print(
|
|
f"{bold('Color:')} {colored('OLD FILE - (Currently in Capstone)', old_color)}\n"
|
|
)
|
|
print(separator_line_1())
|
|
for line in diff_lines:
|
|
if line[0] == "+":
|
|
print(colored(line, new_color))
|
|
elif line[0] == "-":
|
|
print(colored(line, old_color))
|
|
elif line[0] == "?":
|
|
continue
|
|
else:
|
|
print(line)
|
|
print(separator_line_2())
|
|
|
|
@staticmethod
|
|
def no_difference(diff_lines: list[str]) -> bool:
|
|
for line in diff_lines:
|
|
if line[0] != " ":
|
|
return False
|
|
return True
|
|
|
|
def print_prompt(
|
|
self, saved_diff_present: bool = False, saved_choice: ApplyType = None
|
|
) -> str:
|
|
new_color = self.conf_general["diff_color_new"]
|
|
old_color = self.conf_general["diff_color_old"]
|
|
edited_color = self.conf_general["diff_color_edited"]
|
|
saved_selection = self.get_saved_choice_prompt(saved_diff_present, saved_choice)
|
|
|
|
choice = input(
|
|
f"Choice: {colored('O', old_color)}, {bold('o', old_color)}, {bold('n', new_color)}, "
|
|
f"{saved_selection}, {colored('e', edited_color)}, {colored('E', edited_color)}, p, q, ? > "
|
|
)
|
|
return choice
|
|
|
|
def get_saved_choice_prompt(
|
|
self, saved_diff_present: bool = False, saved_choice: ApplyType = None
|
|
):
|
|
new_color = self.conf_general["diff_color_new"]
|
|
old_color = self.conf_general["diff_color_old"]
|
|
edited_color = self.conf_general["diff_color_edited"]
|
|
saved_color = (
|
|
self.conf_general["diff_color_saved"] if saved_diff_present else "dark_grey"
|
|
)
|
|
saved_selection = f"{bold('s', saved_color)}"
|
|
if saved_choice == ApplyType.OLD:
|
|
saved_selection += f" ({colored('old', old_color)}) "
|
|
elif saved_choice == ApplyType.NEW:
|
|
saved_selection += f" ({colored('new', new_color)}) "
|
|
elif saved_choice == ApplyType.EDIT:
|
|
saved_selection += f" ({colored('edited', edited_color)}) "
|
|
elif not saved_choice:
|
|
saved_selection += f" ({colored('none', 'dark_grey')}) "
|
|
return saved_selection
|
|
|
|
def print_prompt_help(
|
|
self, saved_diff_present: bool = False, saved_choice: ApplyType = None
|
|
) -> None:
|
|
new_color = self.conf_general["diff_color_new"]
|
|
old_color = self.conf_general["diff_color_old"]
|
|
edited_color = self.conf_general["diff_color_edited"]
|
|
saved_choice = self.get_saved_choice_prompt(saved_diff_present, saved_choice)
|
|
|
|
print(
|
|
f"{colored('O', old_color)}\t\t- Accept ALL old diffs\n"
|
|
f"{bold('o', old_color)}\t\t- Accept old diff\n"
|
|
f"{bold('n', new_color)}\t\t- Accept new diff\n"
|
|
f"{colored('e', edited_color)}\t\t- Edit diff (not yet implemented)\n"
|
|
f"{saved_choice}\t- Select saved choice\n"
|
|
f"p\t\t- Ignore and go to previous diff\n"
|
|
f"q\t\t- Quit (previous selections will be saved)\n"
|
|
f"?\t\t- Show this help\n\n"
|
|
)
|
|
|
|
def get_user_choice(
|
|
self, saved_diff_present: bool, saved_choice: ApplyType
|
|
) -> ApplyType:
|
|
while True:
|
|
if len(self.user_choices) > 0:
|
|
choice = self.user_choices.pop(0)
|
|
else:
|
|
choice = self.print_prompt(saved_diff_present, saved_choice)
|
|
if choice not in ["O", "o", "n", "e", "E", "s", "p", "q", "?", "help"]:
|
|
print(f"{bold(choice)} is not valid.")
|
|
self.print_prompt_help(saved_diff_present, saved_choice)
|
|
continue
|
|
|
|
if choice == "q":
|
|
print(f"{bold('Quit...')}")
|
|
self.save_to_persistence_file()
|
|
exit(0)
|
|
elif choice == "o":
|
|
return ApplyType.OLD
|
|
elif choice == "n":
|
|
return ApplyType.NEW
|
|
elif choice == "O":
|
|
return ApplyType.OLD_ALL
|
|
elif choice == "e":
|
|
return ApplyType.EDIT
|
|
elif choice == "E":
|
|
return ApplyType.SHOW_EDIT
|
|
elif choice == "s":
|
|
return ApplyType.SAVED
|
|
elif choice in ["?", "help"]:
|
|
self.print_prompt_help(saved_diff_present, saved_choice)
|
|
continue
|
|
elif choice == "p":
|
|
return ApplyType.PREVIOUS
|
|
|
|
def saved_patch_matches(self, saved: dict) -> bool:
|
|
if self.cur_old_node:
|
|
old_hash = get_sha256(self.cur_old_node.text)
|
|
else:
|
|
old_hash = ""
|
|
if self.cur_new_node:
|
|
new_hash = get_sha256(self.cur_new_node.text)
|
|
else:
|
|
new_hash = ""
|
|
return saved["old_hash"] == old_hash and saved["new_hash"] == new_hash
|
|
|
|
def create_patch(
|
|
self,
|
|
coord: PatchCoord,
|
|
choice: ApplyType,
|
|
saved_patch: dict = None,
|
|
edited_text: bytes = None,
|
|
):
|
|
old = self.cur_old_node.text if self.cur_old_node else b""
|
|
new = self.cur_new_node.text if self.cur_new_node else b""
|
|
return Patch(
|
|
self.cur_nid,
|
|
old,
|
|
new,
|
|
coord,
|
|
saved_patch["apply_type"] if saved_patch else choice,
|
|
edit=edited_text,
|
|
)
|
|
|
|
def add_patch(
|
|
self,
|
|
apply_type: ApplyType,
|
|
old_filepath: Path,
|
|
patch_coord: PatchCoord,
|
|
saved_patch: dict | None = None,
|
|
edited_text: bytes | None = None,
|
|
) -> None:
|
|
patch = self.create_patch(patch_coord, apply_type, saved_patch, edited_text)
|
|
self.persist_patch(old_filepath, patch)
|
|
self.patches.append(patch)
|
|
|
|
def diff_nodes(
|
|
self,
|
|
old_filepath: Path,
|
|
new_nodes: dict[str, Node],
|
|
old_nodes: dict[str, Node],
|
|
) -> list[Patch]:
|
|
"""
|
|
Asks the user for each different node, which version should be written.
|
|
It saves the choice to a file, so the previous choice can be applied again if nothing changed.
|
|
"""
|
|
# Sort list of nodes descending.
|
|
# This is necessary because
|
|
# a) we need to apply the patches backwards (starting from the end of the file,
|
|
# so the coordinates in the file don't change when replacing text).
|
|
# b) If there is an old node, which is not present in the new file, we search for
|
|
# a node which is adjacent (random node order wouldn't allow this).
|
|
new_nodes = {
|
|
k: v
|
|
for k, v in sorted(
|
|
new_nodes.items(), key=lambda item: item[1].start_byte, reverse=True
|
|
)
|
|
}
|
|
old_nodes = {
|
|
k: v
|
|
for k, v in sorted(
|
|
old_nodes.items(), key=lambda item: item[1].start_byte, reverse=True
|
|
)
|
|
}
|
|
|
|
# Collect all node ids of this file
|
|
node_ids = set()
|
|
for n in list(new_nodes.keys()) + list(old_nodes.keys()):
|
|
node_ids.add(n)
|
|
|
|
node_ids = sorted(node_ids)
|
|
self.patches = list()
|
|
matching_nodes_count = 0
|
|
choice: ApplyType | None = None
|
|
idx = 0
|
|
while idx < len(node_ids):
|
|
self.cur_nid: str = node_ids[idx]
|
|
self.cur_new_node = (
|
|
None if self.cur_nid not in new_nodes else new_nodes[self.cur_nid]
|
|
)
|
|
self.cur_old_node = (
|
|
None if self.cur_nid not in old_nodes else old_nodes[self.cur_nid]
|
|
)
|
|
|
|
new_node_text = (
|
|
self.cur_new_node.text.decode("utf8").splitlines()
|
|
if self.cur_new_node
|
|
else [""]
|
|
)
|
|
old_node_text = (
|
|
self.cur_old_node.text.decode("utf8").splitlines()
|
|
if self.cur_old_node
|
|
else [""]
|
|
)
|
|
|
|
diff_lines = list(self.differ.compare(old_node_text, new_node_text))
|
|
if self.no_difference(diff_lines):
|
|
log.info(
|
|
f"{bold('Patch:')} {idx + 1}/{len(node_ids)} - Nodes {bold(self.cur_nid)} match."
|
|
)
|
|
matching_nodes_count += 1
|
|
idx += 1
|
|
continue
|
|
|
|
patch_coord = self.determine_patch_coordinates(new_nodes, old_nodes)
|
|
|
|
save_exists = False
|
|
saved: dict | None = None
|
|
if (
|
|
old_filepath.name in self.saved_patches
|
|
and self.cur_nid in self.saved_patches[old_filepath.name]
|
|
):
|
|
saved = self.saved_patches[old_filepath.name][self.cur_nid]
|
|
save_exists = True
|
|
if self.saved_patch_matches(saved) and not self.no_auto_apply:
|
|
apply_type = ApplyType(saved["apply_type"])
|
|
self.add_patch(apply_type, old_filepath, patch_coord)
|
|
log.info(
|
|
f"{bold('Patch:')} {idx + 1}/{len(node_ids)} - Auto apply patch for {bold(self.cur_nid)}"
|
|
)
|
|
idx += 1
|
|
continue
|
|
|
|
if choice == ApplyType.OLD_ALL:
|
|
self.add_patch(ApplyType.OLD, old_filepath, patch_coord)
|
|
idx += 1
|
|
continue
|
|
|
|
self.print_diff(diff_lines, self.cur_nid, idx + 1, len(node_ids))
|
|
choice = self.get_user_choice(
|
|
save_exists, None if not saved else saved["apply_type"]
|
|
)
|
|
if choice == ApplyType.OLD:
|
|
if not self.cur_old_node:
|
|
# No data in old node. Skip
|
|
idx += 1
|
|
continue
|
|
self.add_patch(ApplyType.OLD, old_filepath, patch_coord)
|
|
elif choice == ApplyType.NEW:
|
|
if not self.cur_new_node:
|
|
# No data in old node. Skip
|
|
idx += 1
|
|
continue
|
|
self.add_patch(ApplyType.NEW, old_filepath, patch_coord)
|
|
elif choice == ApplyType.SAVED:
|
|
if not save_exists:
|
|
print(bold("Save does not exist."))
|
|
continue
|
|
self.add_patch(
|
|
saved["apply_type"],
|
|
old_filepath,
|
|
patch_coord,
|
|
saved_patch=saved,
|
|
edited_text=saved["edit"].encode(),
|
|
)
|
|
elif choice == ApplyType.SHOW_EDIT:
|
|
if not saved or not saved["edit"]:
|
|
print(bold("No edited text was saved before."))
|
|
input("Press enter to continue...\n")
|
|
continue
|
|
saved_edited_text = colored(
|
|
f'\n{saved["edit"]}\n', self.conf_general["diff_color_edited"]
|
|
)
|
|
print(saved_edited_text)
|
|
input("Press enter to continue...\n")
|
|
continue
|
|
elif choice == ApplyType.OLD_ALL:
|
|
self.add_patch(ApplyType.OLD, old_filepath, patch_coord)
|
|
elif choice == ApplyType.EDIT:
|
|
edited_text = self.edit_patch(diff_lines)
|
|
if not edited_text:
|
|
continue
|
|
self.add_patch(
|
|
ApplyType.EDIT,
|
|
old_filepath,
|
|
patch_coord,
|
|
edited_text=edited_text,
|
|
)
|
|
elif choice == ApplyType.PREVIOUS:
|
|
if idx == 0:
|
|
print(bold(f"There is no previous diff for {old_filepath.name}!"))
|
|
input("Press enter...")
|
|
continue
|
|
idx -= 1
|
|
continue
|
|
idx += 1
|
|
log.info(f"Number of matching nodes = {matching_nodes_count}")
|
|
return self.patches
|
|
|
|
def determine_patch_coordinates(
|
|
self, new_nodes: dict[str, Node], old_nodes: dict[str, Node]
|
|
):
|
|
# The node to be replaced is Node-PATCHED and part of File-PATCHED
|
|
# The other node is called Node-SRC and is part of File-SRC
|
|
|
|
if self.edit_old_file and self.cur_old_node:
|
|
# File-PATCHED = old file. Because we patch the old file.
|
|
patch_coord = PatchCoord.get_coordinates_from_node(self.cur_old_node)
|
|
return patch_coord
|
|
elif not self.edit_old_file and self.cur_new_node:
|
|
# File-PATCHED = new file. Because we patch the new file.
|
|
patch_coord = PatchCoord.get_coordinates_from_node(self.cur_new_node)
|
|
return patch_coord
|
|
|
|
# In this case there is no Node-PATCHED. So we don't know the PatchCoordinates.
|
|
# We have to insert Node-SRC at another point in the File-PATCHED.
|
|
# If the Node-SRC has no equivalent node in the FILE-PATCHED,
|
|
# we search for another Node-SRC also existing in File-PATCHED.
|
|
# The Node-SRC is insert before the equivalent node in File-PATCHED.
|
|
#
|
|
# Example:
|
|
#
|
|
# File-PATCHED | File-SRC
|
|
# -------------------|-----------------
|
|
# ... | ...
|
|
# <none> | Node-SRC
|
|
# Node-PATCHED-fcn() | Node-SRC-fcn()
|
|
# ... | ...
|
|
#
|
|
# Node-SRC is inserted before Node-PATCHED-fcn(). Because these
|
|
# are the nodes both files share.
|
|
|
|
patch_nodes = old_nodes if self.edit_old_file else new_nodes
|
|
src_nodes = new_nodes if self.edit_old_file else old_nodes
|
|
|
|
src_node_ids = list(src_nodes.keys())
|
|
j = src_node_ids.index(self.cur_nid)
|
|
while j >= 0 and (src_node_ids[j] not in patch_nodes.keys()):
|
|
j -= 1
|
|
|
|
if j < 0 or src_node_ids[j] not in patch_nodes.keys():
|
|
# No shared node before Node-SRC as reference.
|
|
# So put it to the very beginning of the file.
|
|
# The user should move it.
|
|
ref_end_byte = 1
|
|
ref_start_point = (1, 0)
|
|
else:
|
|
ref_new: Node = patch_nodes[src_node_ids[j]]
|
|
ref_end_byte = ref_new.start_byte
|
|
ref_start_point = ref_new.start_point
|
|
patch_coord = PatchCoord(
|
|
ref_end_byte - 1,
|
|
ref_end_byte - 1,
|
|
ref_start_point,
|
|
ref_start_point,
|
|
inserted=True,
|
|
)
|
|
return patch_coord
|
|
|
|
def diff(self, user_choices: list[str] | None = None) -> None:
|
|
"""
|
|
Diffs certain nodes from the newly translated and old source files to each other.
|
|
The user then selects which diff should be written to the new file.
|
|
"""
|
|
|
|
if user_choices:
|
|
self.user_choices = user_choices
|
|
# We do not write to the translated files directly.
|
|
self.copy_files()
|
|
new_file = dict()
|
|
old_file = dict()
|
|
i = 0
|
|
for old_filepath, new_filepath in zip(self.old_files, self.translated_files):
|
|
new_file[i] = dict()
|
|
new_file[i]["filepath"] = new_filepath
|
|
new_file[i]["nodes"] = self.parse_file(new_filepath)
|
|
|
|
old_file[i] = dict()
|
|
old_file[i]["filepath"] = old_filepath
|
|
old_file[i]["nodes"] = self.parse_file(old_filepath)
|
|
i += 1
|
|
|
|
patches = dict()
|
|
# diff each file
|
|
for k in range(i):
|
|
old_filepath = old_file[k]["filepath"]
|
|
new_filepath = new_file[k]["filepath"]
|
|
diffs_to_process = max(len(new_file[k]["nodes"]), len(old_file[k]["nodes"]))
|
|
print_prominent_info(
|
|
self.get_diff_intro_msg(
|
|
old_filepath, new_filepath, k + 1, i, diffs_to_process
|
|
),
|
|
wait_for_user=(not self.testing),
|
|
)
|
|
if diffs_to_process == 0:
|
|
continue
|
|
patch_file_path = (
|
|
old_filepath.name if self.edit_old_file else new_filepath.name
|
|
)
|
|
patches[self.patched_file_paths[patch_file_path]] = self.diff_nodes(
|
|
old_filepath, new_file[k]["nodes"], old_file[k]["nodes"]
|
|
)
|
|
self.patch_files(patches)
|
|
self.save_to_persistence_file()
|
|
log.info("Done")
|
|
|
|
def patch_files(self, file_patches: dict[Path, list[Patch]]) -> None:
|
|
log.info("Write patches...")
|
|
for filepath, patches in file_patches.items():
|
|
patches = sorted(patches, reverse=True)
|
|
with open(filepath, "rb") as f:
|
|
src = f.read()
|
|
patch: Patch
|
|
for patch in patches:
|
|
start_byte = patch.coord.start_byte
|
|
end_byte = patch.coord.end_byte
|
|
if patch.apply == ApplyType.OLD:
|
|
data = patch.old
|
|
elif patch.apply == ApplyType.NEW:
|
|
data = patch.new
|
|
elif patch.apply == ApplyType.EDIT:
|
|
data = patch.edit
|
|
else:
|
|
print_prominent_warning(f"No data for {patch.apply} defined.")
|
|
return
|
|
if patch.coord.inserted:
|
|
# The patch doesn't replace a previous node.
|
|
# So we wrap it in new lines to make it easier to fix later.
|
|
data = b"\n" + data + b"\n"
|
|
prefix = src[:start_byte] if src[:start_byte] else b""
|
|
middle = data if data else b""
|
|
postfix = src[end_byte:] if src[end_byte:] else b""
|
|
src = prefix + middle + postfix
|
|
with open(filepath, "wb") as f:
|
|
f.write(src)
|
|
if not self.testing:
|
|
run_clang_format(list(file_patches.keys()))
|
|
return
|
|
|
|
def edit_patch(self, diff_lines: list[str]) -> bytes | None:
|
|
tmp_file = tempfile.NamedTemporaryFile(suffix="c", delete=False)
|
|
tmp_file_name = tmp_file.name
|
|
tmp_file.writelines([line.encode() + b"\n" for line in diff_lines])
|
|
tmp_file.write(self.get_edit_explanation())
|
|
tmp_file.close()
|
|
editor = self.conf_general["patch_editor"]
|
|
try:
|
|
subprocess.run([editor, tmp_file_name])
|
|
except FileNotFoundError:
|
|
log.error(f"Could not find editor '{editor}'")
|
|
return None
|
|
edited_text = b""
|
|
with open(tmp_file_name, "rb") as tmp_file:
|
|
for line in tmp_file.readlines():
|
|
if self.get_separator_line() in line:
|
|
break
|
|
edited_text += line
|
|
return edited_text
|
|
|
|
@staticmethod
|
|
def get_separator_line() -> bytes:
|
|
return f"// {'=' * 50}".encode()
|
|
|
|
def get_edit_explanation(self) -> bytes:
|
|
return (
|
|
f"{self.get_separator_line().decode('utf8')}\n"
|
|
"// Everything below this line will be deleted\n"
|
|
"// Edit the file to your liking. The result will be written 'as is' to the source file.\n"
|
|
).encode()
|
|
|
|
def check_saved_patches(self):
|
|
new_file = dict()
|
|
old_file = dict()
|
|
i = 0
|
|
for old_filepath, new_filepath in zip(self.old_files, self.translated_files):
|
|
new_file[i] = {
|
|
"filepath": new_filepath,
|
|
"nodes": self.parse_file(new_filepath),
|
|
}
|
|
old_file[i] = {
|
|
"filepath": old_filepath,
|
|
"nodes": self.parse_file(old_filepath),
|
|
}
|
|
i += 1
|
|
|
|
# diff each file
|
|
for k in range(i):
|
|
old_filepath = old_file[k]["filepath"]
|
|
diffs_to_process = max(len(new_file[k]["nodes"]), len(old_file[k]["nodes"]))
|
|
if diffs_to_process == 0:
|
|
continue
|
|
filename, node_id = self.all_choices_saved(
|
|
old_filepath, new_file[k]["nodes"], old_file[k]["nodes"]
|
|
)
|
|
if not node_id:
|
|
# Edge case of file has all nodes matching. And therefore has
|
|
# no decision saved because the user never was asked.
|
|
continue
|
|
if filename or node_id:
|
|
print(
|
|
f"{get_path('{DIFFER_PERSISTENCE_FILE}').name} is not up-to-date!\n"
|
|
f"{filename} still requires a user decision for node {node_id}.\n"
|
|
f"If the file is good as it is, "
|
|
f"commit any changes to it and run the Differ again and choose 'O' to save them."
|
|
)
|
|
exit(1)
|
|
|
|
def all_choices_saved(
|
|
self, old_filepath, new_nodes, old_nodes
|
|
) -> tuple[str, str] | tuple[None, None]:
|
|
"""Returns the a (filename, node_id) which is not saved in the save file. Or None, if everything is ok."""
|
|
if old_filepath.name not in self.saved_patches:
|
|
return old_filepath.name, None
|
|
|
|
new_nodes = {
|
|
k: v
|
|
for k, v in sorted(
|
|
new_nodes.items(), key=lambda item: item[1].start_byte, reverse=True
|
|
)
|
|
}
|
|
old_nodes = {
|
|
k: v
|
|
for k, v in sorted(
|
|
old_nodes.items(), key=lambda item: item[1].start_byte, reverse=True
|
|
)
|
|
}
|
|
|
|
# Collect all node ids of this file
|
|
node_ids = set()
|
|
for new_node_id, old_node_id in zip(new_nodes.keys(), old_nodes.keys()):
|
|
node_ids.add(new_node_id)
|
|
node_ids.add(old_node_id)
|
|
|
|
for self.cur_nid in node_ids:
|
|
self.cur_new_node = (
|
|
None if self.cur_nid not in new_nodes else new_nodes[self.cur_nid]
|
|
)
|
|
self.cur_old_node = (
|
|
None if self.cur_nid not in old_nodes else old_nodes[self.cur_nid]
|
|
)
|
|
if (
|
|
old_filepath.name in self.saved_patches
|
|
and self.cur_nid in self.saved_patches[old_filepath.name]
|
|
):
|
|
saved = self.saved_patches[old_filepath.name][self.cur_nid]
|
|
if not self.saved_patch_matches(saved):
|
|
return old_filepath.name, self.cur_nid
|
|
return None, None
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
prog="Differ",
|
|
description="Diffs translated C++ files to previous version.",
|
|
)
|
|
parser.add_argument(
|
|
"-e",
|
|
dest="no_auto_apply",
|
|
help="Do not apply saved diff resolutions. Ask for every diff again.",
|
|
action="store_true",
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
dest="arch",
|
|
help="Name of target architecture (ignored with -t option)",
|
|
choices=ARCH_LLVM_NAMING,
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
"-v",
|
|
dest="verbosity",
|
|
help="Verbosity of the log messages.",
|
|
choices=["debug", "info", "warning", "fatal"],
|
|
default="info",
|
|
)
|
|
parser.add_argument(
|
|
"-t", dest="testing", help="Run with test configuration.", action="store_true"
|
|
)
|
|
parser.add_argument(
|
|
"--check_saved",
|
|
dest="check_saved",
|
|
help=f"Check if patches in {get_path('{DIFFER_PERSISTENCE_FILE}')} is up-to-date.",
|
|
action="store_true",
|
|
)
|
|
arguments = parser.parse_args()
|
|
return arguments
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = parse_args()
|
|
log.basicConfig(
|
|
level=convert_loglevel(args.verbosity),
|
|
stream=sys.stdout,
|
|
format="%(levelname)-5s - %(message)s",
|
|
)
|
|
if args.testing:
|
|
cfg = Configurator("ARCH", get_path("{DIFFER_TEST_CONFIG_FILE}"))
|
|
else:
|
|
cfg = Configurator(args.arch, get_path("{CPP_TRANSLATOR_CONFIG}"))
|
|
|
|
differ = Differ(
|
|
cfg, args.no_auto_apply, testing=args.testing, check_saved=args.check_saved
|
|
)
|
|
if args.check_saved:
|
|
exit(0)
|
|
|
|
try:
|
|
differ.diff()
|
|
except Exception as e:
|
|
differ.save_to_persistence_file()
|
|
raise e
|