|  | #!/usr/bin/env python3 | 
|  | # SPDX-License-Identifier: GPL-2.0 | 
|  |  | 
|  | """ | 
|  | This script helps track the translation status of the documentation | 
|  | in different locales, e.g., zh_CN. More specially, it uses `git log` | 
|  | commit to find the latest english commit from the translation commit | 
|  | (order by author date) and the latest english commits from HEAD. If | 
|  | differences occur, report the file and commits that need to be updated. | 
|  |  | 
|  | The usage is as follows: | 
|  | - ./scripts/checktransupdate.py -l zh_CN | 
|  | This will print all the files that need to be updated or translated in the zh_CN locale. | 
|  | - ./scripts/checktransupdate.py Documentation/translations/zh_CN/dev-tools/testing-overview.rst | 
|  | This will only print the status of the specified file. | 
|  |  | 
|  | The output is something like: | 
|  | Documentation/dev-tools/kfence.rst | 
|  | No translation in the locale of zh_CN | 
|  |  | 
|  | Documentation/translations/zh_CN/dev-tools/testing-overview.rst | 
|  | commit 42fb9cfd5b18 ("Documentation: dev-tools: Add link to RV docs") | 
|  | 1 commits needs resolving in total | 
|  | """ | 
|  |  | 
|  | import os | 
|  | import time | 
|  | import logging | 
|  | from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction | 
|  | from datetime import datetime | 
|  |  | 
|  |  | 
|  | def get_origin_path(file_path): | 
|  | """Get the origin path from the translation path""" | 
|  | paths = file_path.split("/") | 
|  | tidx = paths.index("translations") | 
|  | opaths = paths[:tidx] | 
|  | opaths += paths[tidx + 2 :] | 
|  | return "/".join(opaths) | 
|  |  | 
|  |  | 
|  | def get_latest_commit_from(file_path, commit): | 
|  | """Get the latest commit from the specified commit for the specified file""" | 
|  | command = f"git log --pretty=format:%H%n%aD%n%cD%n%n%B {commit} -1 -- {file_path}" | 
|  | logging.debug(command) | 
|  | pipe = os.popen(command) | 
|  | result = pipe.read() | 
|  | result = result.split("\n") | 
|  | if len(result) <= 1: | 
|  | return None | 
|  |  | 
|  | logging.debug("Result: %s", result[0]) | 
|  |  | 
|  | return { | 
|  | "hash": result[0], | 
|  | "author_date": datetime.strptime(result[1], "%a, %d %b %Y %H:%M:%S %z"), | 
|  | "commit_date": datetime.strptime(result[2], "%a, %d %b %Y %H:%M:%S %z"), | 
|  | "message": result[4:], | 
|  | } | 
|  |  | 
|  |  | 
|  | def get_origin_from_trans(origin_path, t_from_head): | 
|  | """Get the latest origin commit from the translation commit""" | 
|  | o_from_t = get_latest_commit_from(origin_path, t_from_head["hash"]) | 
|  | while o_from_t is not None and o_from_t["author_date"] > t_from_head["author_date"]: | 
|  | o_from_t = get_latest_commit_from(origin_path, o_from_t["hash"] + "^") | 
|  | if o_from_t is not None: | 
|  | logging.debug("tracked origin commit id: %s", o_from_t["hash"]) | 
|  | return o_from_t | 
|  |  | 
|  |  | 
|  | def get_commits_count_between(opath, commit1, commit2): | 
|  | """Get the commits count between two commits for the specified file""" | 
|  | command = f"git log --pretty=format:%H {commit1}...{commit2} -- {opath}" | 
|  | logging.debug(command) | 
|  | pipe = os.popen(command) | 
|  | result = pipe.read().split("\n") | 
|  | # filter out empty lines | 
|  | result = list(filter(lambda x: x != "", result)) | 
|  | return result | 
|  |  | 
|  |  | 
|  | def pretty_output(commit): | 
|  | """Pretty print the commit message""" | 
|  | command = f"git log --pretty='format:%h (\"%s\")' -1 {commit}" | 
|  | logging.debug(command) | 
|  | pipe = os.popen(command) | 
|  | return pipe.read() | 
|  |  | 
|  |  | 
|  | def valid_commit(commit): | 
|  | """Check if the commit is valid or not""" | 
|  | msg = pretty_output(commit) | 
|  | return "Merge tag" not in msg | 
|  |  | 
|  | def check_per_file(file_path): | 
|  | """Check the translation status for the specified file""" | 
|  | opath = get_origin_path(file_path) | 
|  |  | 
|  | if not os.path.isfile(opath): | 
|  | logging.error("Cannot find the origin path for {file_path}") | 
|  | return | 
|  |  | 
|  | o_from_head = get_latest_commit_from(opath, "HEAD") | 
|  | t_from_head = get_latest_commit_from(file_path, "HEAD") | 
|  |  | 
|  | if o_from_head is None or t_from_head is None: | 
|  | logging.error("Cannot find the latest commit for %s", file_path) | 
|  | return | 
|  |  | 
|  | o_from_t = get_origin_from_trans(opath, t_from_head) | 
|  |  | 
|  | if o_from_t is None: | 
|  | logging.error("Error: Cannot find the latest origin commit for %s", file_path) | 
|  | return | 
|  |  | 
|  | if o_from_head["hash"] == o_from_t["hash"]: | 
|  | logging.debug("No update needed for %s", file_path) | 
|  | else: | 
|  | logging.info(file_path) | 
|  | commits = get_commits_count_between( | 
|  | opath, o_from_t["hash"], o_from_head["hash"] | 
|  | ) | 
|  | count = 0 | 
|  | for commit in commits: | 
|  | if valid_commit(commit): | 
|  | logging.info("commit %s", pretty_output(commit)) | 
|  | count += 1 | 
|  | logging.info("%d commits needs resolving in total\n", count) | 
|  |  | 
|  |  | 
|  | def valid_locales(locale): | 
|  | """Check if the locale is valid or not""" | 
|  | script_path = os.path.dirname(os.path.abspath(__file__)) | 
|  | linux_path = os.path.join(script_path, "..") | 
|  | if not os.path.isdir(f"{linux_path}/Documentation/translations/{locale}"): | 
|  | raise ArgumentTypeError("Invalid locale: {locale}") | 
|  | return locale | 
|  |  | 
|  |  | 
|  | def list_files_with_excluding_folders(folder, exclude_folders, include_suffix): | 
|  | """List all files with the specified suffix in the folder and its subfolders""" | 
|  | files = [] | 
|  | stack = [folder] | 
|  |  | 
|  | while stack: | 
|  | pwd = stack.pop() | 
|  | # filter out the exclude folders | 
|  | if os.path.basename(pwd) in exclude_folders: | 
|  | continue | 
|  | # list all files and folders | 
|  | for item in os.listdir(pwd): | 
|  | ab_item = os.path.join(pwd, item) | 
|  | if os.path.isdir(ab_item): | 
|  | stack.append(ab_item) | 
|  | else: | 
|  | if ab_item.endswith(include_suffix): | 
|  | files.append(ab_item) | 
|  |  | 
|  | return files | 
|  |  | 
|  |  | 
|  | class DmesgFormatter(logging.Formatter): | 
|  | """Custom dmesg logging formatter""" | 
|  | def format(self, record): | 
|  | timestamp = time.time() | 
|  | formatted_time = f"[{timestamp:>10.6f}]" | 
|  | log_message = f"{formatted_time} {record.getMessage()}" | 
|  | return log_message | 
|  |  | 
|  |  | 
|  | def config_logging(log_level, log_file="checktransupdate.log"): | 
|  | """configure logging based on the log level""" | 
|  | # set up the root logger | 
|  | logger = logging.getLogger() | 
|  | logger.setLevel(log_level) | 
|  |  | 
|  | # Create console handler | 
|  | console_handler = logging.StreamHandler() | 
|  | console_handler.setLevel(log_level) | 
|  |  | 
|  | # Create file handler | 
|  | file_handler = logging.FileHandler(log_file) | 
|  | file_handler.setLevel(log_level) | 
|  |  | 
|  | # Create formatter and add it to the handlers | 
|  | formatter = DmesgFormatter() | 
|  | console_handler.setFormatter(formatter) | 
|  | file_handler.setFormatter(formatter) | 
|  |  | 
|  | # Add the handler to the logger | 
|  | logger.addHandler(console_handler) | 
|  | logger.addHandler(file_handler) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | """Main function of the script""" | 
|  | script_path = os.path.dirname(os.path.abspath(__file__)) | 
|  | linux_path = os.path.join(script_path, "..") | 
|  |  | 
|  | parser = ArgumentParser(description="Check the translation update") | 
|  | parser.add_argument( | 
|  | "-l", | 
|  | "--locale", | 
|  | default="zh_CN", | 
|  | type=valid_locales, | 
|  | help="Locale to check when files are not specified", | 
|  | ) | 
|  |  | 
|  | parser.add_argument( | 
|  | "--print-missing-translations", | 
|  | action=BooleanOptionalAction, | 
|  | default=True, | 
|  | help="Print files that do not have translations", | 
|  | ) | 
|  |  | 
|  | parser.add_argument( | 
|  | '--log', | 
|  | default='INFO', | 
|  | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], | 
|  | help='Set the logging level') | 
|  |  | 
|  | parser.add_argument( | 
|  | '--logfile', | 
|  | default='checktransupdate.log', | 
|  | help='Set the logging file (default: checktransupdate.log)') | 
|  |  | 
|  | parser.add_argument( | 
|  | "files", nargs="*", help="Files to check, if not specified, check all files" | 
|  | ) | 
|  | args = parser.parse_args() | 
|  |  | 
|  | # Configure logging based on the --log argument | 
|  | log_level = getattr(logging, args.log.upper(), logging.INFO) | 
|  | config_logging(log_level) | 
|  |  | 
|  | # Get files related to linux path | 
|  | files = args.files | 
|  | if len(files) == 0: | 
|  | offical_files = list_files_with_excluding_folders( | 
|  | os.path.join(linux_path, "Documentation"), ["translations", "output"], "rst" | 
|  | ) | 
|  |  | 
|  | for file in offical_files: | 
|  | # split the path into parts | 
|  | path_parts = file.split(os.sep) | 
|  | # find the index of the "Documentation" directory | 
|  | kindex = path_parts.index("Documentation") | 
|  | # insert the translations and locale after the Documentation directory | 
|  | new_path_parts = path_parts[:kindex + 1] + ["translations", args.locale] \ | 
|  | + path_parts[kindex + 1 :] | 
|  | # join the path parts back together | 
|  | new_file = os.sep.join(new_path_parts) | 
|  | if os.path.isfile(new_file): | 
|  | files.append(new_file) | 
|  | else: | 
|  | if args.print_missing_translations: | 
|  | logging.info(os.path.relpath(os.path.abspath(file), linux_path)) | 
|  | logging.info("No translation in the locale of %s\n", args.locale) | 
|  |  | 
|  | files = list(map(lambda x: os.path.relpath(os.path.abspath(x), linux_path), files)) | 
|  |  | 
|  | # cd to linux root directory | 
|  | os.chdir(linux_path) | 
|  |  | 
|  | for file in files: | 
|  | check_per_file(file) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |