@@ -294,6 +294,7 @@ impl GitRepo {
294294 "-r" ,
295295 "--name-only" ,
296296 "--no-commit-id" ,
297+ "-z" ,
297298 & valid_from,
298299 to_commit,
299300 ] ;
@@ -311,13 +312,19 @@ impl GitRepo {
311312 // Add untracked files or unstaged changes, i.e. files that are not in git at
312313 // all
313314 let ls_files_output = self . execute_git_command (
314- & [ "ls-files" , "--others" , "--modified" , "--exclude-standard" ] ,
315+ & [
316+ "ls-files" ,
317+ "--others" ,
318+ "--modified" ,
319+ "--exclude-standard" ,
320+ "-z" ,
321+ ] ,
315322 pathspec,
316323 ) ?;
317324 self . add_files_from_stdout ( & mut files, turbo_root, ls_files_output) ;
318325 // Include any files that have been staged, but not committed
319326 let diff_output =
320- self . execute_git_command ( & [ "diff" , "--name-only" , "--cached" ] , pathspec) ?;
327+ self . execute_git_command ( & [ "diff" , "--name-only" , "--cached" , "-z" ] , pathspec) ?;
321328 self . add_files_from_stdout ( & mut files, turbo_root, diff_output) ;
322329 }
323330
@@ -351,8 +358,11 @@ impl GitRepo {
351358 turbo_root : & AbsoluteSystemPath ,
352359 stdout : Vec < u8 > ,
353360 ) {
354- let stdout = String :: from_utf8 ( stdout) . unwrap ( ) ;
355- for line in stdout. lines ( ) {
361+ let stdout = String :: from_utf8_lossy ( & stdout) ;
362+ for line in stdout. split ( '\0' ) {
363+ if line. is_empty ( ) {
364+ continue ;
365+ }
356366 let path = RelativeUnixPath :: new ( line) . unwrap ( ) ;
357367 let anchored_to_turbo_root_file_path = self
358368 . reanchor_path_from_git_root_to_turbo_root ( turbo_root, path)
@@ -1133,6 +1143,106 @@ mod tests {
11331143 Ok ( ( ) )
11341144 }
11351145
1146+ #[ test]
1147+ fn test_unicode_filenames_in_changed_files ( ) -> Result < ( ) , Error > {
1148+ let ( repo_root, repo) = setup_repository ( None ) ?;
1149+ let root = AbsoluteSystemPathBuf :: try_from ( repo_root. path ( ) ) . unwrap ( ) ;
1150+
1151+ // Test various Unicode filenames that should be properly handled with -z flag
1152+ let test_files = vec ! [
1153+ "测试文件.txt" , // Chinese
1154+ "テストファイル.js" , // Japanese
1155+ "файл.rs" , // Cyrillic
1156+ "file with spaces.txt" ,
1157+ "emoji_🚀.md" ,
1158+ "café.ts" , // Latin with diacritics
1159+ "ñoño.jsx" , // Spanish with tildes
1160+ "αβγ.py" , // Greek
1161+ ] ;
1162+
1163+ // Create initial commit with a base file
1164+ let base_file = root. join_component ( "base.txt" ) ;
1165+ base_file. create_with_contents ( "base content" ) ?;
1166+ let first_commit_oid = commit_file ( & repo, Path :: new ( "base.txt" ) , None ) ;
1167+
1168+ // Create and commit all Unicode files
1169+ for filename in & test_files {
1170+ let file_path = root. join_component ( filename) ;
1171+ file_path. create_with_contents ( format ! ( "content for {}" , filename) ) ?;
1172+ }
1173+
1174+ // Get changed files with uncommitted Unicode files
1175+ let scm = SCM :: new ( & root) ;
1176+ let files = scm
1177+ . changed_files ( & root, Some ( "HEAD" ) , None , true , false , false ) ?
1178+ . unwrap ( ) ;
1179+
1180+ // Verify all Unicode files are detected in uncommitted changes
1181+ for filename in & test_files {
1182+ assert ! (
1183+ files. iter( ) . any( |f| f. to_string( ) . contains( filename) ) ,
1184+ "Failed to detect uncommitted Unicode file: {}" ,
1185+ filename
1186+ ) ;
1187+ }
1188+
1189+ // Commit all Unicode files
1190+ let mut last_commit = first_commit_oid;
1191+ for filename in & test_files {
1192+ last_commit = commit_file ( & repo, Path :: new ( filename) , Some ( last_commit) ) ;
1193+ }
1194+
1195+ // Test committed Unicode files in range
1196+ let files = changed_files (
1197+ repo_root. path ( ) . to_path_buf ( ) ,
1198+ repo_root. path ( ) . to_path_buf ( ) ,
1199+ Some ( first_commit_oid. to_string ( ) . as_str ( ) ) ,
1200+ Some ( "HEAD" ) ,
1201+ false ,
1202+ ) ?;
1203+
1204+ // Verify all Unicode files are detected in commit range
1205+ for filename in & test_files {
1206+ assert ! (
1207+ files. iter( ) . any( |f| f. contains( filename) ) ,
1208+ "Failed to detect committed Unicode file: {}" ,
1209+ filename
1210+ ) ;
1211+ }
1212+
1213+ // Test modification of Unicode files
1214+ let modified_file = "测试文件.txt" ;
1215+ let file_path = root. join_component ( modified_file) ;
1216+ file_path. create_with_contents ( "modified content" ) ?;
1217+
1218+ let files = scm
1219+ . changed_files ( & root, Some ( "HEAD" ) , None , true , false , false ) ?
1220+ . unwrap ( ) ;
1221+
1222+ assert ! (
1223+ files. iter( ) . any( |f| f. to_string( ) . contains( modified_file) ) ,
1224+ "Failed to detect modified Unicode file: {}" ,
1225+ modified_file
1226+ ) ;
1227+
1228+ // Test deletion of Unicode files
1229+ let delete_file = "emoji_🚀.md" ;
1230+ let file_path = root. join_component ( delete_file) ;
1231+ std:: fs:: remove_file ( file_path. as_std_path ( ) ) ?;
1232+
1233+ let files = scm
1234+ . changed_files ( & root, Some ( "HEAD" ) , None , true , false , false ) ?
1235+ . unwrap ( ) ;
1236+
1237+ assert ! (
1238+ files. iter( ) . any( |f| f. to_string( ) . contains( delete_file) ) ,
1239+ "Failed to detect deleted Unicode file: {}" ,
1240+ delete_file
1241+ ) ;
1242+
1243+ Ok ( ( ) )
1244+ }
1245+
11361246 struct TestCase {
11371247 env : CIEnv ,
11381248 event_json : & ' static str ,
0 commit comments