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