Skip to content

Commit e2f9212

Browse files
committed
tools/check_translation_update.py: check if the translations are up to date
Check if the translations in docs/translations/LANG/FILES are update with docs/FILES with detailed outputs. Translations should be committed with "Update to commit HASH (TITLE)".
1 parent ed351ea commit e2f9212

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

tools/check_translation_update.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
# Usage: python tools/check_translation_update.py
7+
8+
import os
9+
import re
10+
import sys
11+
import argparse
12+
import subprocess
13+
14+
def get_git_repo_root(path):
15+
"""Get root path of the repository"""
16+
try:
17+
# Use git rev-parse --show-toplevel to find the root path
18+
result = subprocess.run(
19+
['git', 'rev-parse', '--show-toplevel'],
20+
cwd=path,
21+
capture_output=True,
22+
text=True,
23+
check=True
24+
)
25+
return result.stdout.strip()
26+
except subprocess.CalledProcessError:
27+
print(f"Error: current work directory {path} is not in a Git repo.")
28+
return None
29+
except FileNotFoundError:
30+
print("Error: 'git' command not found.")
31+
return None
32+
except Exception as e:
33+
print(f"Error: {e}")
34+
return None
35+
36+
def get_commit_time(repo_root, commit_hash):
37+
"""Get the commit time for a given commit hash."""
38+
try:
39+
result = subprocess.run(
40+
['git', 'show', '-s', '--format=%ci', commit_hash],
41+
cwd=repo_root,
42+
capture_output=True,
43+
text=True,
44+
check=True
45+
)
46+
commit_time = result.stdout.strip()
47+
return commit_time.split(' ')[0] + ' ' + commit_time.split(' ')[1]
48+
except Exception as e:
49+
print(f"Error in getting commit time of {commit_hash}: {e}")
50+
return None
51+
52+
def get_latest_commit_info(repo_root, file_path):
53+
"""Get the latest commit hash and message for a given file.
54+
Args:
55+
repo_root: Git repository root path
56+
file_path: Path to the file
57+
Returns:
58+
tuple: (commit_hash, commit_time, commit_message) or (None, None, None) if not found
59+
"""
60+
try:
61+
result = subprocess.run(
62+
['git', 'log', '-1', '--format=%H%n%ci%n%B', '--', file_path],
63+
cwd=repo_root,
64+
capture_output=True,
65+
text=True,
66+
check=True
67+
)
68+
69+
lines = result.stdout.splitlines()
70+
if len(lines) >= 3:
71+
commit_hash = lines[0]
72+
commit_date = lines[1].split(' ')[0] + ' ' + lines[1].split(' ')[1]
73+
commit_message = '\n'.join(lines[2:])
74+
return commit_hash, commit_date, commit_message
75+
76+
return None, None, None
77+
except Exception as e:
78+
print(f"Fail to get latest commit info of {file_path}: {e}")
79+
return None, None, None
80+
81+
def extract_source_commit_info(repo_root, file_path):
82+
"""Extract the source commit hash and time that this translation is based on.
83+
Args:
84+
repo_root: Git repository root path
85+
file_path: Path to the translation file
86+
Returns:
87+
tuple: (source_commit_hash, source_commit_time) or (None, None) if not found
88+
"""
89+
try:
90+
_, _, translation_commit_message = get_latest_commit_info(repo_root, file_path)
91+
92+
update_marker = 'Update to commit'
93+
update_info = ''
94+
source_commit_hash, source_commit_time = None, None
95+
96+
for line in translation_commit_message.splitlines():
97+
if update_marker in line:
98+
update_info = line.strip()
99+
break
100+
101+
match = re.search(r"Update to commit ([0-9a-fA-F]{7,12}) \(\"(.+?)\"\)", update_info)
102+
if match:
103+
source_commit_hash = match.group(1)
104+
source_commit_time = get_commit_time(repo_root, source_commit_hash)
105+
106+
return source_commit_hash, source_commit_time
107+
except Exception as e:
108+
print(f"Fail to extract source commit info of {file_path}: {e}")
109+
return None, None
110+
111+
def extract_translation_language(file_path):
112+
"""Extract the language code from the translation file path."""
113+
match = re.search(r'docs/translations/([^/]+)/', file_path)
114+
if match:
115+
return match.group(1)
116+
return None
117+
118+
def check_translation_update(repo_root, translation_file_path):
119+
"""Check if the translation file is up to date with the original file.
120+
Args:
121+
repo_root: Git repository root path
122+
translation_file_path: Path to the translation file
123+
Returns:
124+
tuple: (is_translation, support_update_check, is_update)
125+
True if the translation supports update check and is up to date, False otherwise
126+
"""
127+
# 1. Checks if it is a valid translation file and needs to be checked
128+
language = extract_translation_language(translation_file_path)
129+
if not os.path.exists(translation_file_path) or language is None or f"docs/translations/{language}/README.md" in translation_file_path:
130+
return False, False, False
131+
132+
# 2. Extract commit info of the translated source file
133+
translated_source_commit_hash, translated_source_commit_time = extract_source_commit_info(repo_root, translation_file_path)
134+
if not translated_source_commit_hash:
135+
print(f"File {translation_file_path} does not have a formatted update commit message, skip it.")
136+
return True, False, False
137+
138+
# 3. Get the latest commit info of the source file
139+
# given the translation file syzkaller/docs/translations/LANGUAGE/PATH/ORIG.md
140+
# then the source file should be syzkaller/docs/PATH/ORIG.md
141+
relative_path = os.path.relpath(translation_file_path, repo_root)
142+
if "docs/translations/" not in relative_path:
143+
print(f"File '{translation_file_path}' is not a translation, skip it.")
144+
return False, False, False
145+
146+
source_file_path = relative_path.replace(f"docs/translations/{language}/", "docs/")
147+
source_file_abs_path = os.path.join(repo_root, source_file_path)
148+
if not os.path.exists(source_file_abs_path):
149+
print(f"Source file '{source_file_abs_path}' does not exist, skip it.")
150+
return True, True, False
151+
source_commit_hash, source_commit_time, _ = get_latest_commit_info(repo_root, source_file_abs_path)
152+
153+
# 4. Compare the commit hashes between the translated source and latest source
154+
if translated_source_commit_hash[:7] != source_commit_hash[:7]:
155+
print(f"{translation_file_path} is based on {translated_source_commit_hash[:7]} ({translated_source_commit_time}), " \
156+
f"while the latest source is {source_commit_hash[:7]} ({source_commit_time}).")
157+
return True, True, False
158+
159+
return True, True, True
160+
161+
def main():
162+
parser = argparse.ArgumentParser(description="Check the update of translation files in syzkaller/docs/translations/.")
163+
parser.add_argument("-f", "--files", nargs="+", help="one or multiple paths of translation files (test only)")
164+
parser.add_argument("-r", "--repo-root", default=".", help="root directory of syzkaller (default: current directory)")
165+
args = parser.parse_args()
166+
167+
repo_root = get_git_repo_root(args.repo_root)
168+
if not repo_root:
169+
return
170+
171+
total_cnt, support_update_check_cnt, is_update_cnt = 0, 0, 0
172+
173+
if args.files:
174+
for file_path in args.files:
175+
abs_file_path = os.path.abspath(file_path)
176+
if not abs_file_path.startswith(repo_root):
177+
print(f"File '{file_path}' is not in {repo_root}', skip it.")
178+
continue
179+
180+
is_translation, support_update_check, is_update = check_translation_update(repo_root, abs_file_path)
181+
total_cnt += int(is_translation)
182+
support_update_check_cnt += int(support_update_check)
183+
is_update_cnt += int(is_update)
184+
print(f"Summary: {support_update_check_cnt}/{total_cnt} translation files have formatted commit message that support update check, " \
185+
f"{is_update_cnt}/{support_update_check_cnt} are update to date.")
186+
sys.exit(0)
187+
188+
translation_dir = os.path.join(repo_root, 'docs', 'translations')
189+
for root, _, files in os.walk(translation_dir):
190+
for file in files:
191+
translation_path = os.path.join(root, file)
192+
# print(f"[DEBUG] {translation_path}")
193+
is_translation, support_update_check, is_update = check_translation_update(repo_root, translation_path)
194+
total_cnt += int(is_translation)
195+
support_update_check_cnt += int(support_update_check)
196+
is_update_cnt += int(is_update)
197+
print(f"Summary: {support_update_check_cnt}/{total_cnt} translation files have formatted commit message that support update check, " \
198+
f"{is_update_cnt}/{support_update_check_cnt} are update to date.")
199+
sys.exit(0)
200+
# We will add other exit code once all the previous translation commit messages are unified with the new format.
201+
202+
if __name__ == "__main__":
203+
main()

0 commit comments

Comments
 (0)