1- use std:: path:: Path ;
1+ use std:: path:: { Path , PathBuf } ;
22
33use anyhow:: { Context , Result , bail} ;
4- use tokio:: process:: Command ;
4+ use tokio:: { fs , process:: Command } ;
55use yazi_shared:: strip_trailing_newline;
6+ use yazi_shim:: wtf8:: { FromWtf8 , FromWtf8Vec } ;
67
78pub ( super ) struct Git ;
89
910impl Git {
1011 pub ( super ) async fn clone ( url : & str , path : & Path ) -> Result < ( ) > {
11- Self :: exec ( |c| c. args ( [ "clone" , url] ) . arg ( path) ) . await
12+ Self :: exec ( |c| c. args ( [ "clone" , url] ) . arg ( path) ) . await ?;
13+ Self :: materialize ( path) . await
1214 }
1315
1416 pub ( super ) async fn fetch ( path : & Path ) -> Result < ( ) > {
1517 Self :: exec ( |c| c. arg ( "fetch" ) . current_dir ( path) ) . await
1618 }
1719
1820 pub ( super ) async fn checkout ( path : & Path , rev : & str ) -> Result < ( ) > {
19- Self :: exec ( |c| c. args ( [ "checkout" , rev, "--force" ] ) . current_dir ( path) ) . await
21+ Self :: exec ( |c| c. args ( [ "checkout" , rev, "--force" ] ) . current_dir ( path) ) . await ?;
22+ Self :: materialize ( path) . await
2023 }
2124
2225 pub ( super ) async fn pull ( path : & Path ) -> Result < ( ) > {
@@ -42,13 +45,55 @@ impl Git {
4245 ) )
4346 }
4447
48+ async fn materialize ( path : & Path ) -> Result < ( ) > {
49+ let path = fs:: canonicalize ( path) . await . context ( "Failed to resolve Git repository" ) ?;
50+ let output = Command :: new ( "git" )
51+ . args ( [ "ls-files" , "--stage" , "-z" ] )
52+ . current_dir ( & path)
53+ . output ( )
54+ . await
55+ . context ( "Failed to list Git files" ) ?;
56+ if !output. status . success ( ) {
57+ bail ! ( "Listing Git files failed: {}" , output. status) ;
58+ }
59+
60+ for ent in output. stdout . split ( |& c| c == 0 ) . filter ( |b| !b. is_empty ( ) ) {
61+ if !ent. starts_with ( b"120000 " ) {
62+ continue ;
63+ }
64+
65+ let Some ( tab) = ent. iter ( ) . position ( |& b| b == b'\t' ) else { continue } ;
66+ let link = path. join (
67+ Path :: from_wtf8 ( & ent[ tab + 1 ..] ) . context ( "Git path cannot be represented by the OS" ) ?,
68+ ) ;
69+
70+ let original = PathBuf :: from_wtf8_vec ( fs:: read ( & link) . await ?)
71+ . context ( "Git symlink origin cannot be represented by the OS" ) ?;
72+ let original = fs:: canonicalize ( link. parent ( ) . unwrap_or ( & path) . join ( original) )
73+ . await
74+ . with_context ( || format ! ( "failed to resolve Git symlink target `{}`" , link. display( ) ) ) ?;
75+
76+ if !original. starts_with ( & path) {
77+ bail ! ( "Git symlink target escapes repository: `{}`" , link. display( ) ) ;
78+ }
79+
80+ fs:: copy ( original, & link)
81+ . await
82+ . with_context ( || format ! ( "failed to materialize `{}`" , link. display( ) ) ) ?;
83+ }
84+
85+ Ok ( ( ) )
86+ }
87+
4588 async fn exec ( f : impl FnOnce ( & mut Command ) -> & mut Command ) -> Result < ( ) > {
4689 let status = f ( Command :: new ( "git" ) . args ( [
4790 "-c" ,
4891 "core.eol=lf" ,
4992 "-c" ,
5093 "core.autocrlf=false" ,
5194 "-c" ,
95+ "core.symlinks=false" ,
96+ "-c" ,
5297 "clone.defaultRemoteName=origin" ,
5398 "-c" ,
5499 "checkout.defaultRemote=origin" ,
0 commit comments