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