github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/tools/check_translation_update.py (about) 1 #!/usr/bin/env python 2 # Copyright 2025 syzkaller project authors. All rights reserved. 3 # Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 4 # Contributed by QGrain <zhiyuzhang999@gmail.com> 5 6 # Intro: Due to the continuous updates of the docs, we would like to know if the translations are up to 7 # date with the source docs. This script checks the translation files in docs/translations/ by tracking 8 # the commit hash of the source file, which requires the formatted line "Update to commit HASH (TITLE)" 9 # to be present in the commit message of the translation file. 10 11 # Usage: python tools/check_translation_update.py 12 13 import os 14 import re 15 import sys 16 import argparse 17 import subprocess 18 19 def get_git_repo_root(path): 20 """Get root path of the repository""" 21 try: 22 # Use git rev-parse --show-toplevel to find the root path (disable shell to avoid potential shell injection) 23 result = subprocess.run( 24 ['git', 'rev-parse', '--show-toplevel'], 25 cwd=path, 26 capture_output=True, 27 text=True, 28 check=True 29 ) 30 return result.stdout.strip() 31 except subprocess.CalledProcessError: 32 print(f"Error: current work directory {path} is not in a Git repo.") 33 return None 34 except FileNotFoundError: 35 print("Error: 'git' command not found.") 36 return None 37 except Exception as e: 38 print(f"Error: {e}") 39 return None 40 41 def get_commit_date(repo_root, commit_hash): 42 """Get the commit date (YYYY-MM-DD hh:mm:ss) for a given commit hash.""" 43 try: 44 result = subprocess.run( 45 ['git', 'show', '-s', '--format=%ci', commit_hash], 46 cwd=repo_root, 47 capture_output=True, 48 text=True, 49 check=True 50 ) 51 raw_commit_date = result.stdout.strip() 52 return extract_compact_date(raw_commit_date) 53 except Exception as e: 54 print(f"Error in getting commit time of {commit_hash}: {e}") 55 return None 56 57 def get_latest_commit_info(repo_root, file_path): 58 """Get the latest commit hash and message for a given file. 59 Args: 60 repo_root: Git repository root path 61 file_path: Path to the file 62 Returns: 63 tuple: (commit_hash, commit_date, commit_message) or (None, None, None) if not found 64 """ 65 try: 66 result = subprocess.run( 67 ['git', 'log', '-1', '--format=%H%n%ci%n%B', '--', file_path], 68 cwd=repo_root, 69 capture_output=True, 70 text=True, 71 check=True 72 ) 73 74 lines = result.stdout.splitlines() 75 if len(lines) >= 3: 76 commit_hash = lines[0] 77 commit_date = extract_compact_date(lines[1]) 78 commit_message = '\n'.join(lines[2:]) 79 return commit_hash, commit_date, commit_message 80 81 return None, None, None 82 except Exception as e: 83 print(f"Fail to get latest commit info of {file_path}: {e}") 84 return None, None, None 85 86 def extract_source_commit_info(repo_root, file_path): 87 """Extract the source commit hash and date that this translation is based on. 88 Args: 89 repo_root: Git repository root path 90 file_path: Path to the translation file 91 Returns: 92 tuple: (source_commit_hash, source_commit_date) or (None, None) if not found 93 """ 94 try: 95 _, _, translation_commit_message = get_latest_commit_info(repo_root, file_path) 96 97 update_marker = 'Update to commit' 98 update_info = '' 99 source_commit_hash, source_commit_date = None, None 100 101 for line in translation_commit_message.splitlines(): 102 if update_marker in line: 103 update_info = line.strip() 104 break 105 106 match = re.search(r"Update to commit ([0-9a-fA-F]{7,12}) \(\"(.+?)\"\)", update_info) 107 if match: 108 source_commit_hash = match.group(1) 109 source_commit_date = get_commit_date(repo_root, source_commit_hash) 110 111 return source_commit_hash, source_commit_date 112 except Exception as e: 113 print(f"Fail to extract source commit info of {file_path}: {e}") 114 return None, None 115 116 def extract_translation_language(file_path): 117 """Extract the language code from the translation file path.""" 118 match = re.search(r'docs/translations/([^/]+)/', file_path) 119 if match: 120 return match.group(1) 121 return None 122 123 def check_translation_update(repo_root, translation_file_path): 124 """Check if the translation file is up to date with the source file. 125 Args: 126 repo_root: Git repository root path 127 translation_file_path: Path to the translation file 128 Returns: 129 tuple: (is_translation, support_update_check, is_update) 130 True if the translation supports update check and is up to date, False otherwise 131 """ 132 # 1. Checks if it is a valid translation file and needs to be checked 133 language = extract_translation_language(translation_file_path) 134 if not os.path.exists(translation_file_path) or language is None or f"docs/translations/{language}/README.md" in translation_file_path: 135 return False, False, False 136 137 # 2. Extract commit info of the translated source file 138 translated_source_commit_hash, translated_source_commit_date = extract_source_commit_info(repo_root, translation_file_path) 139 if not translated_source_commit_hash: 140 print(f"File {translation_file_path} does not have a formatted update commit message, skip it.") 141 return True, False, False 142 143 # 3. Get the latest commit info of the source file 144 # given the translation file syzkaller/docs/translations/LANGUAGE/PATH/ORIG.md 145 # then the source file should be syzkaller/docs/PATH/ORIG.md 146 relative_path = os.path.relpath(translation_file_path, repo_root) 147 if "docs/translations/" not in relative_path: 148 print(f"File '{translation_file_path}' is not a translation, skip it.") 149 return False, False, False 150 151 source_file_path = relative_path.replace(f"docs/translations/{language}/", "docs/") 152 source_file_abs_path = os.path.join(repo_root, source_file_path) 153 if not os.path.exists(source_file_abs_path): 154 print(f"Source file '{source_file_abs_path}' does not exist, skip it.") 155 return True, True, False 156 source_commit_hash, source_commit_date, _ = get_latest_commit_info(repo_root, source_file_abs_path) 157 158 # 4. Compare the commit hashes between the translated source and latest source 159 if translated_source_commit_hash[:7] != source_commit_hash[:7]: 160 print(f"{translation_file_path} is based on {translated_source_commit_hash[:7]} ({translated_source_commit_date}), " \ 161 f"while the latest source is {source_commit_hash[:7]} ({source_commit_date}).") 162 return True, True, False 163 164 return True, True, True 165 166 def extract_compact_date(raw_date_str): 167 """Extract a compact date string from a raw date string. 168 Arg: 169 raw_date_str: Raw date string output by '%ci' format: 'YYYY-MM-DD hh:mm:ss ZONE' 170 Return: 171 compact_date_str: Compact date string in format 'YYYY-MM-DD hh:mm:ss' 172 """ 173 compact_date_str = raw_date_str 174 try: 175 parts = raw_date_str.split(' ') 176 compact_date_str = f"{parts[0]} {parts[1]}" 177 except Exception as e: 178 print(f"Fail to extract compact date from {raw_date_str}: {e}") 179 return compact_date_str 180 181 def main(): 182 parser = argparse.ArgumentParser(description="Check the update of translation files in syzkaller/docs/translations/.") 183 parser.add_argument("-f", "--files", nargs="+", help="one or multiple paths of translation files (test only)") 184 parser.add_argument("-r", "--repo-root", default=".", help="root directory of syzkaller (default: current directory)") 185 args = parser.parse_args() 186 187 repo_root = get_git_repo_root(args.repo_root) 188 if not repo_root: 189 return 190 191 total_cnt, support_update_check_cnt, is_update_cnt = 0, 0, 0 192 193 if args.files: 194 for file_path in args.files: 195 abs_file_path = os.path.abspath(file_path) 196 if not abs_file_path.startswith(repo_root): 197 print(f"File '{file_path}' is not in {repo_root}', skip it.") 198 continue 199 200 is_translation, support_update_check, is_update = check_translation_update(repo_root, abs_file_path) 201 total_cnt += int(is_translation) 202 support_update_check_cnt += int(support_update_check) 203 is_update_cnt += int(is_update) 204 print(f"Summary: {support_update_check_cnt}/{total_cnt} translation files have formatted commit message that support update check, " \ 205 f"{is_update_cnt}/{support_update_check_cnt} are update to date.") 206 sys.exit(0) 207 208 translation_dir = os.path.join(repo_root, 'docs', 'translations') 209 for root, _, files in os.walk(translation_dir): 210 for file in files: 211 translation_path = os.path.join(root, file) 212 # print(f"[DEBUG] {translation_path}") 213 is_translation, support_update_check, is_update = check_translation_update(repo_root, translation_path) 214 total_cnt += int(is_translation) 215 support_update_check_cnt += int(support_update_check) 216 is_update_cnt += int(is_update) 217 print(f"Summary: {support_update_check_cnt}/{total_cnt} translation files have formatted commit message that support update check, " \ 218 f"{is_update_cnt}/{support_update_check_cnt} are update to date.") 219 sys.exit(0) 220 # We will add other exit code once all the previous translation commit messages are unified with the new format. 221 222 if __name__ == "__main__": 223 main()