150c0fa7fSDongliang Mu#!/usr/bin/env python3 250c0fa7fSDongliang Mu# SPDX-License-Identifier: GPL-2.0 350c0fa7fSDongliang Mu 450c0fa7fSDongliang Mu""" 550c0fa7fSDongliang MuThis script helps track the translation status of the documentation 650c0fa7fSDongliang Muin different locales, e.g., zh_CN. More specially, it uses `git log` 750c0fa7fSDongliang Mucommit to find the latest english commit from the translation commit 850c0fa7fSDongliang Mu(order by author date) and the latest english commits from HEAD. If 950c0fa7fSDongliang Mudifferences occur, report the file and commits that need to be updated. 1050c0fa7fSDongliang Mu 1150c0fa7fSDongliang MuThe usage is as follows: 1250c0fa7fSDongliang Mu- ./scripts/checktransupdate.py -l zh_CN 13*63e96ce0SDongliang MuThis will print all the files that need to be updated or translated in the zh_CN locale. 1450c0fa7fSDongliang Mu- ./scripts/checktransupdate.py Documentation/translations/zh_CN/dev-tools/testing-overview.rst 1550c0fa7fSDongliang MuThis will only print the status of the specified file. 1650c0fa7fSDongliang Mu 1750c0fa7fSDongliang MuThe output is something like: 18*63e96ce0SDongliang MuDocumentation/dev-tools/kfence.rst 19*63e96ce0SDongliang MuNo translation in the locale of zh_CN 20*63e96ce0SDongliang Mu 21*63e96ce0SDongliang MuDocumentation/translations/zh_CN/dev-tools/testing-overview.rst 2250c0fa7fSDongliang Mucommit 42fb9cfd5b18 ("Documentation: dev-tools: Add link to RV docs") 23*63e96ce0SDongliang Mu1 commits needs resolving in total 2450c0fa7fSDongliang Mu""" 2550c0fa7fSDongliang Mu 2650c0fa7fSDongliang Muimport os 27*63e96ce0SDongliang Muimport time 28*63e96ce0SDongliang Muimport logging 29*63e96ce0SDongliang Mufrom argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction 3050c0fa7fSDongliang Mufrom datetime import datetime 3150c0fa7fSDongliang Mu 3250c0fa7fSDongliang Mu 3350c0fa7fSDongliang Mudef get_origin_path(file_path): 34*63e96ce0SDongliang Mu """Get the origin path from the translation path""" 3550c0fa7fSDongliang Mu paths = file_path.split("/") 3650c0fa7fSDongliang Mu tidx = paths.index("translations") 3750c0fa7fSDongliang Mu opaths = paths[:tidx] 3850c0fa7fSDongliang Mu opaths += paths[tidx + 2 :] 3950c0fa7fSDongliang Mu return "/".join(opaths) 4050c0fa7fSDongliang Mu 4150c0fa7fSDongliang Mu 4250c0fa7fSDongliang Mudef get_latest_commit_from(file_path, commit): 43*63e96ce0SDongliang Mu """Get the latest commit from the specified commit for the specified file""" 44*63e96ce0SDongliang Mu command = f"git log --pretty=format:%H%n%aD%n%cD%n%n%B {commit} -1 -- {file_path}" 45*63e96ce0SDongliang Mu logging.debug(command) 4650c0fa7fSDongliang Mu pipe = os.popen(command) 4750c0fa7fSDongliang Mu result = pipe.read() 4850c0fa7fSDongliang Mu result = result.split("\n") 4950c0fa7fSDongliang Mu if len(result) <= 1: 5050c0fa7fSDongliang Mu return None 5150c0fa7fSDongliang Mu 52*63e96ce0SDongliang Mu logging.debug("Result: %s", result[0]) 5350c0fa7fSDongliang Mu 5450c0fa7fSDongliang Mu return { 5550c0fa7fSDongliang Mu "hash": result[0], 5650c0fa7fSDongliang Mu "author_date": datetime.strptime(result[1], "%a, %d %b %Y %H:%M:%S %z"), 5750c0fa7fSDongliang Mu "commit_date": datetime.strptime(result[2], "%a, %d %b %Y %H:%M:%S %z"), 5850c0fa7fSDongliang Mu "message": result[4:], 5950c0fa7fSDongliang Mu } 6050c0fa7fSDongliang Mu 6150c0fa7fSDongliang Mu 6250c0fa7fSDongliang Mudef get_origin_from_trans(origin_path, t_from_head): 63*63e96ce0SDongliang Mu """Get the latest origin commit from the translation commit""" 6450c0fa7fSDongliang Mu o_from_t = get_latest_commit_from(origin_path, t_from_head["hash"]) 6550c0fa7fSDongliang Mu while o_from_t is not None and o_from_t["author_date"] > t_from_head["author_date"]: 6650c0fa7fSDongliang Mu o_from_t = get_latest_commit_from(origin_path, o_from_t["hash"] + "^") 6750c0fa7fSDongliang Mu if o_from_t is not None: 68*63e96ce0SDongliang Mu logging.debug("tracked origin commit id: %s", o_from_t["hash"]) 6950c0fa7fSDongliang Mu return o_from_t 7050c0fa7fSDongliang Mu 7150c0fa7fSDongliang Mu 7250c0fa7fSDongliang Mudef get_commits_count_between(opath, commit1, commit2): 73*63e96ce0SDongliang Mu """Get the commits count between two commits for the specified file""" 74*63e96ce0SDongliang Mu command = f"git log --pretty=format:%H {commit1}...{commit2} -- {opath}" 75*63e96ce0SDongliang Mu logging.debug(command) 7650c0fa7fSDongliang Mu pipe = os.popen(command) 7750c0fa7fSDongliang Mu result = pipe.read().split("\n") 7850c0fa7fSDongliang Mu # filter out empty lines 7950c0fa7fSDongliang Mu result = list(filter(lambda x: x != "", result)) 8050c0fa7fSDongliang Mu return result 8150c0fa7fSDongliang Mu 8250c0fa7fSDongliang Mu 8350c0fa7fSDongliang Mudef pretty_output(commit): 84*63e96ce0SDongliang Mu """Pretty print the commit message""" 85*63e96ce0SDongliang Mu command = f"git log --pretty='format:%h (\"%s\")' -1 {commit}" 86*63e96ce0SDongliang Mu logging.debug(command) 8750c0fa7fSDongliang Mu pipe = os.popen(command) 8850c0fa7fSDongliang Mu return pipe.read() 8950c0fa7fSDongliang Mu 9050c0fa7fSDongliang Mu 91*63e96ce0SDongliang Mudef valid_commit(commit): 92*63e96ce0SDongliang Mu """Check if the commit is valid or not""" 93*63e96ce0SDongliang Mu msg = pretty_output(commit) 94*63e96ce0SDongliang Mu return "Merge tag" not in msg 95*63e96ce0SDongliang Mu 9650c0fa7fSDongliang Mudef check_per_file(file_path): 97*63e96ce0SDongliang Mu """Check the translation status for the specified file""" 9850c0fa7fSDongliang Mu opath = get_origin_path(file_path) 9950c0fa7fSDongliang Mu 10050c0fa7fSDongliang Mu if not os.path.isfile(opath): 101*63e96ce0SDongliang Mu logging.error("Cannot find the origin path for {file_path}") 10250c0fa7fSDongliang Mu return 10350c0fa7fSDongliang Mu 10450c0fa7fSDongliang Mu o_from_head = get_latest_commit_from(opath, "HEAD") 10550c0fa7fSDongliang Mu t_from_head = get_latest_commit_from(file_path, "HEAD") 10650c0fa7fSDongliang Mu 10750c0fa7fSDongliang Mu if o_from_head is None or t_from_head is None: 108*63e96ce0SDongliang Mu logging.error("Cannot find the latest commit for %s", file_path) 10950c0fa7fSDongliang Mu return 11050c0fa7fSDongliang Mu 11150c0fa7fSDongliang Mu o_from_t = get_origin_from_trans(opath, t_from_head) 11250c0fa7fSDongliang Mu 11350c0fa7fSDongliang Mu if o_from_t is None: 114*63e96ce0SDongliang Mu logging.error("Error: Cannot find the latest origin commit for %s", file_path) 11550c0fa7fSDongliang Mu return 11650c0fa7fSDongliang Mu 11750c0fa7fSDongliang Mu if o_from_head["hash"] == o_from_t["hash"]: 118*63e96ce0SDongliang Mu logging.debug("No update needed for %s", file_path) 11950c0fa7fSDongliang Mu else: 120*63e96ce0SDongliang Mu logging.info(file_path) 12150c0fa7fSDongliang Mu commits = get_commits_count_between( 12250c0fa7fSDongliang Mu opath, o_from_t["hash"], o_from_head["hash"] 12350c0fa7fSDongliang Mu ) 124*63e96ce0SDongliang Mu count = 0 12550c0fa7fSDongliang Mu for commit in commits: 126*63e96ce0SDongliang Mu if valid_commit(commit): 127*63e96ce0SDongliang Mu logging.info("commit %s", pretty_output(commit)) 128*63e96ce0SDongliang Mu count += 1 129*63e96ce0SDongliang Mu logging.info("%d commits needs resolving in total\n", count) 130*63e96ce0SDongliang Mu 131*63e96ce0SDongliang Mu 132*63e96ce0SDongliang Mudef valid_locales(locale): 133*63e96ce0SDongliang Mu """Check if the locale is valid or not""" 134*63e96ce0SDongliang Mu script_path = os.path.dirname(os.path.abspath(__file__)) 135*63e96ce0SDongliang Mu linux_path = os.path.join(script_path, "..") 136*63e96ce0SDongliang Mu if not os.path.isdir(f"{linux_path}/Documentation/translations/{locale}"): 137*63e96ce0SDongliang Mu raise ArgumentTypeError("Invalid locale: {locale}") 138*63e96ce0SDongliang Mu return locale 139*63e96ce0SDongliang Mu 140*63e96ce0SDongliang Mu 141*63e96ce0SDongliang Mudef list_files_with_excluding_folders(folder, exclude_folders, include_suffix): 142*63e96ce0SDongliang Mu """List all files with the specified suffix in the folder and its subfolders""" 143*63e96ce0SDongliang Mu files = [] 144*63e96ce0SDongliang Mu stack = [folder] 145*63e96ce0SDongliang Mu 146*63e96ce0SDongliang Mu while stack: 147*63e96ce0SDongliang Mu pwd = stack.pop() 148*63e96ce0SDongliang Mu # filter out the exclude folders 149*63e96ce0SDongliang Mu if os.path.basename(pwd) in exclude_folders: 150*63e96ce0SDongliang Mu continue 151*63e96ce0SDongliang Mu # list all files and folders 152*63e96ce0SDongliang Mu for item in os.listdir(pwd): 153*63e96ce0SDongliang Mu ab_item = os.path.join(pwd, item) 154*63e96ce0SDongliang Mu if os.path.isdir(ab_item): 155*63e96ce0SDongliang Mu stack.append(ab_item) 156*63e96ce0SDongliang Mu else: 157*63e96ce0SDongliang Mu if ab_item.endswith(include_suffix): 158*63e96ce0SDongliang Mu files.append(ab_item) 159*63e96ce0SDongliang Mu 160*63e96ce0SDongliang Mu return files 161*63e96ce0SDongliang Mu 162*63e96ce0SDongliang Mu 163*63e96ce0SDongliang Muclass DmesgFormatter(logging.Formatter): 164*63e96ce0SDongliang Mu """Custom dmesg logging formatter""" 165*63e96ce0SDongliang Mu def format(self, record): 166*63e96ce0SDongliang Mu timestamp = time.time() 167*63e96ce0SDongliang Mu formatted_time = f"[{timestamp:>10.6f}]" 168*63e96ce0SDongliang Mu log_message = f"{formatted_time} {record.getMessage()}" 169*63e96ce0SDongliang Mu return log_message 170*63e96ce0SDongliang Mu 171*63e96ce0SDongliang Mu 172*63e96ce0SDongliang Mudef config_logging(log_level, log_file="checktransupdate.log"): 173*63e96ce0SDongliang Mu """configure logging based on the log level""" 174*63e96ce0SDongliang Mu # set up the root logger 175*63e96ce0SDongliang Mu logger = logging.getLogger() 176*63e96ce0SDongliang Mu logger.setLevel(log_level) 177*63e96ce0SDongliang Mu 178*63e96ce0SDongliang Mu # Create console handler 179*63e96ce0SDongliang Mu console_handler = logging.StreamHandler() 180*63e96ce0SDongliang Mu console_handler.setLevel(log_level) 181*63e96ce0SDongliang Mu 182*63e96ce0SDongliang Mu # Create file handler 183*63e96ce0SDongliang Mu file_handler = logging.FileHandler(log_file) 184*63e96ce0SDongliang Mu file_handler.setLevel(log_level) 185*63e96ce0SDongliang Mu 186*63e96ce0SDongliang Mu # Create formatter and add it to the handlers 187*63e96ce0SDongliang Mu formatter = DmesgFormatter() 188*63e96ce0SDongliang Mu console_handler.setFormatter(formatter) 189*63e96ce0SDongliang Mu file_handler.setFormatter(formatter) 190*63e96ce0SDongliang Mu 191*63e96ce0SDongliang Mu # Add the handler to the logger 192*63e96ce0SDongliang Mu logger.addHandler(console_handler) 193*63e96ce0SDongliang Mu logger.addHandler(file_handler) 19450c0fa7fSDongliang Mu 19550c0fa7fSDongliang Mu 19650c0fa7fSDongliang Mudef main(): 197*63e96ce0SDongliang Mu """Main function of the script""" 19850c0fa7fSDongliang Mu script_path = os.path.dirname(os.path.abspath(__file__)) 19950c0fa7fSDongliang Mu linux_path = os.path.join(script_path, "..") 20050c0fa7fSDongliang Mu 20150c0fa7fSDongliang Mu parser = ArgumentParser(description="Check the translation update") 20250c0fa7fSDongliang Mu parser.add_argument( 20350c0fa7fSDongliang Mu "-l", 20450c0fa7fSDongliang Mu "--locale", 205*63e96ce0SDongliang Mu default="zh_CN", 206*63e96ce0SDongliang Mu type=valid_locales, 20750c0fa7fSDongliang Mu help="Locale to check when files are not specified", 20850c0fa7fSDongliang Mu ) 209*63e96ce0SDongliang Mu 21050c0fa7fSDongliang Mu parser.add_argument( 211*63e96ce0SDongliang Mu "--print-missing-translations", 21250c0fa7fSDongliang Mu action=BooleanOptionalAction, 21350c0fa7fSDongliang Mu default=True, 214*63e96ce0SDongliang Mu help="Print files that do not have translations", 21550c0fa7fSDongliang Mu ) 21650c0fa7fSDongliang Mu 21750c0fa7fSDongliang Mu parser.add_argument( 218*63e96ce0SDongliang Mu '--log', 219*63e96ce0SDongliang Mu default='INFO', 220*63e96ce0SDongliang Mu choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], 221*63e96ce0SDongliang Mu help='Set the logging level') 22250c0fa7fSDongliang Mu 22350c0fa7fSDongliang Mu parser.add_argument( 224*63e96ce0SDongliang Mu '--logfile', 225*63e96ce0SDongliang Mu default='checktransupdate.log', 226*63e96ce0SDongliang Mu help='Set the logging file (default: checktransupdate.log)') 22750c0fa7fSDongliang Mu 22850c0fa7fSDongliang Mu parser.add_argument( 22950c0fa7fSDongliang Mu "files", nargs="*", help="Files to check, if not specified, check all files" 23050c0fa7fSDongliang Mu ) 23150c0fa7fSDongliang Mu args = parser.parse_args() 23250c0fa7fSDongliang Mu 233*63e96ce0SDongliang Mu # Configure logging based on the --log argument 234*63e96ce0SDongliang Mu log_level = getattr(logging, args.log.upper(), logging.INFO) 235*63e96ce0SDongliang Mu config_logging(log_level) 23650c0fa7fSDongliang Mu 237*63e96ce0SDongliang Mu # Get files related to linux path 23850c0fa7fSDongliang Mu files = args.files 23950c0fa7fSDongliang Mu if len(files) == 0: 240*63e96ce0SDongliang Mu offical_files = list_files_with_excluding_folders( 241*63e96ce0SDongliang Mu os.path.join(linux_path, "Documentation"), ["translations", "output"], "rst" 24250c0fa7fSDongliang Mu ) 24350c0fa7fSDongliang Mu 244*63e96ce0SDongliang Mu for file in offical_files: 245*63e96ce0SDongliang Mu # split the path into parts 246*63e96ce0SDongliang Mu path_parts = file.split(os.sep) 247*63e96ce0SDongliang Mu # find the index of the "Documentation" directory 248*63e96ce0SDongliang Mu kindex = path_parts.index("Documentation") 249*63e96ce0SDongliang Mu # insert the translations and locale after the Documentation directory 250*63e96ce0SDongliang Mu new_path_parts = path_parts[:kindex + 1] + ["translations", args.locale] \ 251*63e96ce0SDongliang Mu + path_parts[kindex + 1 :] 252*63e96ce0SDongliang Mu # join the path parts back together 253*63e96ce0SDongliang Mu new_file = os.sep.join(new_path_parts) 254*63e96ce0SDongliang Mu if os.path.isfile(new_file): 255*63e96ce0SDongliang Mu files.append(new_file) 256*63e96ce0SDongliang Mu else: 257*63e96ce0SDongliang Mu if args.print_missing_translations: 258*63e96ce0SDongliang Mu logging.info(os.path.relpath(os.path.abspath(file), linux_path)) 259*63e96ce0SDongliang Mu logging.info("No translation in the locale of %s\n", args.locale) 260*63e96ce0SDongliang Mu 26150c0fa7fSDongliang Mu files = list(map(lambda x: os.path.relpath(os.path.abspath(x), linux_path), files)) 26250c0fa7fSDongliang Mu 26350c0fa7fSDongliang Mu # cd to linux root directory 26450c0fa7fSDongliang Mu os.chdir(linux_path) 26550c0fa7fSDongliang Mu 26650c0fa7fSDongliang Mu for file in files: 26750c0fa7fSDongliang Mu check_per_file(file) 26850c0fa7fSDongliang Mu 26950c0fa7fSDongliang Mu 27050c0fa7fSDongliang Muif __name__ == "__main__": 27150c0fa7fSDongliang Mu main() 272