1515use std:: fs:: File ;
1616#[ cfg( unix) ]
1717use std:: os:: unix:: fs:: PermissionsExt as _;
18+ use std:: path:: Component ;
1819use std:: path:: Path ;
1920use std:: path:: PathBuf ;
2021use std:: sync:: Arc ;
@@ -27,6 +28,7 @@ use itertools::Itertools as _;
2728use jj_lib:: backend:: CopyId ;
2829use jj_lib:: backend:: TreeId ;
2930use jj_lib:: backend:: TreeValue ;
31+ use jj_lib:: file_util;
3032use jj_lib:: file_util:: check_symlink_support;
3133use jj_lib:: file_util:: try_symlink;
3234use jj_lib:: fsmonitor:: FsmonitorSettings ;
@@ -2141,3 +2143,176 @@ fn test_snapshot_max_new_file_size() {
21412143 UntrackedReason :: FileTooLarge { .. }
21422144 ) ;
21432145}
2146+
2147+ #[ test]
2148+ fn test_snapshot_symlink_use_forward_slash ( ) {
2149+ if !file_util:: check_symlink_support ( ) . unwrap ( ) {
2150+ eprintln ! ( "Symlink not supported. Skip the test." ) ;
2151+ }
2152+ let mut test_workspace = TestWorkspace :: init ( ) ;
2153+ let workspace_root = test_workspace. workspace . workspace_root ( ) . to_owned ( ) ;
2154+ let target = repo_path ( "target/link/target.txt" ) ;
2155+ let target_path = target. to_fs_path ( & workspace_root) . unwrap ( ) ;
2156+ std:: fs:: create_dir_all ( target_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
2157+ std:: fs:: write ( & target_path, "a\n " ) . unwrap ( ) ;
2158+ let link = repo_path ( "link/link.txt" ) ;
2159+ let link_path = link. to_fs_path ( & workspace_root) . unwrap ( ) ;
2160+ let link_contents = "../target/link/target.txt" ;
2161+ std:: fs:: create_dir_all ( link_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
2162+ file_util:: try_symlink ( link_contents, link_path) . unwrap ( ) ;
2163+
2164+ let tree = test_workspace
2165+ . snapshot ( )
2166+ . expect ( "Snapshot with symlink should succeed." ) ;
2167+ let tree_value = tree
2168+ . path_value ( link)
2169+ . expect ( "Failed to retrieve the MergedTreeValue from the path." )
2170+ . into_resolved ( )
2171+ . expect ( "Shouldn't have conflicts." )
2172+ . expect ( "The link path should exist." ) ;
2173+ let TreeValue :: Symlink ( symlink_id) = tree_value. clone ( ) else {
2174+ panic ! (
2175+ "Expect {} to be a symlink, but got {:?}" ,
2176+ link. as_internal_file_string( ) ,
2177+ tree_value
2178+ ) ;
2179+ } ;
2180+ let actual_link_contents = test_workspace
2181+ . repo
2182+ . store ( )
2183+ . read_symlink ( link, & symlink_id)
2184+ . block_on ( )
2185+ . unwrap ( ) ;
2186+
2187+ assert ! (
2188+ !actual_link_contents. contains( "\\ " ) ,
2189+ "Expect the symlink in the Store to use \" /\" as the separator, but got \
2190+ {actual_link_contents}."
2191+ ) ;
2192+ }
2193+
2194+ fn is_verbatim_path ( path : & Path ) -> bool {
2195+ let Some ( Component :: Prefix ( prefix) ) = path. components ( ) . next ( ) else {
2196+ return false ;
2197+ } ;
2198+ prefix. kind ( ) . is_verbatim ( )
2199+ }
2200+
2201+ #[ cfg( windows) ]
2202+ fn absolute_path_to_verbatim_path ( input : & Path ) -> PathBuf {
2203+ use std:: ffi:: OsString ;
2204+ use std:: path:: Prefix ;
2205+
2206+ use bstr:: ByteSlice as _;
2207+
2208+ assert ! ( input. is_absolute( ) ) ;
2209+ let input = input. canonicalize ( ) . unwrap ( ) ;
2210+
2211+ let mut components = input. components ( ) ;
2212+ let Component :: Prefix ( prefix_component) = components. next ( ) . unwrap ( ) else {
2213+ panic ! ( "target should be an absolute path after being canonicalized" ) ;
2214+ } ;
2215+ let mut verbatim_path = match prefix_component. kind ( ) {
2216+ // C: -> \\?\Global\C:
2217+ // \\?\C: -> \\?\Global\C:
2218+ //
2219+ // Prefix the path with Global, so that when we read back the symlink, it's still a verbatim
2220+ // path. The symlink to a \\?\C: prefixed path(e.g., \\?\C:\file.txt) will be converted to a
2221+ // not verbatim path(e.g., C:\file.txt) when calling read_link.
2222+ Prefix :: Disk ( disk) | Prefix :: VerbatimDisk ( disk) => {
2223+ let mut verbatim_prefix = OsString :: from ( r"\\?\Global\" ) ;
2224+ verbatim_prefix. push ( [ disk] . to_os_str ( ) . unwrap ( ) ) ;
2225+ verbatim_prefix. push ( ":" ) ;
2226+ verbatim_prefix
2227+ }
2228+ _ => panic ! ( "Unsupported path: {}" , input. display( ) ) ,
2229+ } ;
2230+ verbatim_path. push ( components. as_path ( ) . as_os_str ( ) ) ;
2231+ let verbatim_path = PathBuf :: from ( verbatim_path) ;
2232+ assert ! ( is_verbatim_path( & verbatim_path) ) ;
2233+ verbatim_path
2234+ }
2235+
2236+ #[ test_case( |link, target| file_util:: relative_path( link. parent( ) . unwrap( ) , target) ; "relative" ) ]
2237+ #[ test_case( |_, target| {
2238+ assert!( target. is_absolute( ) ) ;
2239+ target. to_owned( )
2240+ } ; "absolute" ) ]
2241+ #[ cfg_attr(
2242+ windows,
2243+ test_case( |_, target: & Path | absolute_path_to_verbatim_path( target) ; "verbatim absolute" )
2244+ ) ]
2245+ fn test_snapshot_and_update_valid_symlink ( get_link_target : impl FnOnce ( & Path , & Path ) -> PathBuf ) {
2246+ if !file_util:: check_symlink_support ( ) . unwrap ( ) {
2247+ eprintln ! ( "Symlink not supported. Skip the test." ) ;
2248+ }
2249+ let mut test_workspace = TestWorkspace :: init ( ) ;
2250+ let workspace_root = test_workspace. workspace . workspace_root ( ) . to_owned ( ) ;
2251+ let target = repo_path ( "target/link/target.txt" ) ;
2252+ let target_path = target. to_fs_path ( & workspace_root) . unwrap ( ) ;
2253+ std:: fs:: create_dir_all ( target_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
2254+ // Unique contents that it's unlikely that we match accidentally.
2255+ let file_contents = b"18bHZD165T@C\n " ;
2256+ std:: fs:: write ( & target_path, file_contents) . unwrap ( ) ;
2257+ let link = repo_path ( "link/link.txt" ) ;
2258+ let link_path = link. to_fs_path ( & workspace_root) . unwrap ( ) ;
2259+ let link_contents = get_link_target ( & link_path, & target_path) ;
2260+ std:: fs:: create_dir_all ( link_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
2261+ file_util:: try_symlink ( & link_contents, & link_path) . unwrap ( ) ;
2262+ std:: fs:: read_link ( & link_path) . expect ( "The symlink itself should exist." ) ;
2263+ assert_eq ! ( std:: fs:: read( & link_path) . unwrap( ) , file_contents) ;
2264+ assert_eq ! (
2265+ is_verbatim_path( & std:: fs:: read_link( & link_path) . unwrap( ) ) ,
2266+ is_verbatim_path( & link_contents) ,
2267+ "Make sure that when we test with a verbatim path, it's still a verbatim path in the \
2268+ Store when snapshotting."
2269+ ) ;
2270+
2271+ let tree = test_workspace
2272+ . snapshot ( )
2273+ . expect ( "Snapshot with symlink should succeed." ) ;
2274+ let commit = commit_with_tree ( test_workspace. repo . store ( ) , tree) ;
2275+
2276+ // Checkout the root commit to clear the workspace.
2277+ let mut locked_ws = test_workspace
2278+ . workspace
2279+ . start_working_copy_mutation ( )
2280+ . unwrap ( ) ;
2281+ let root_commit = test_workspace. repo . store ( ) . root_commit ( ) ;
2282+ locked_ws
2283+ . locked_wc ( )
2284+ . check_out ( & root_commit)
2285+ . block_on ( )
2286+ . unwrap ( ) ;
2287+ locked_ws
2288+ . finish ( test_workspace. repo . op_id ( ) . clone ( ) )
2289+ . unwrap ( ) ;
2290+
2291+ assert ! ( !std:: fs:: exists( & link_path) . unwrap( ) ) ;
2292+ assert ! ( std:: fs:: read_link( & link_path) . is_err( ) ) ;
2293+
2294+ // Checkout the original commit back.
2295+ let mut locked_ws = test_workspace
2296+ . workspace
2297+ . start_working_copy_mutation ( )
2298+ . unwrap ( ) ;
2299+ locked_ws. locked_wc ( ) . check_out ( & commit) . block_on ( ) . unwrap ( ) ;
2300+ locked_ws
2301+ . finish ( test_workspace. repo . op_id ( ) . clone ( ) )
2302+ . unwrap ( ) ;
2303+
2304+ let actual_target = std:: fs:: read_link ( & link_path) . expect ( "The symlink itself should exist." ) ;
2305+ let actual_contents = std:: fs:: read ( & link_path) . unwrap_or_else ( |e| {
2306+ panic ! (
2307+ "Failed to read from the symlink at {}, which points to {}: {e:?}" ,
2308+ link_path. display( ) ,
2309+ actual_target. display( )
2310+ )
2311+ } ) ;
2312+ assert_eq ! ( actual_contents, file_contents) ;
2313+ assert_eq ! (
2314+ is_verbatim_path( & std:: fs:: read_link( & link_path) . unwrap( ) ) ,
2315+ is_verbatim_path( & link_contents) ,
2316+ "When we checkout a symlink to a verbatim path, it should still point to a verbatim path."
2317+ ) ;
2318+ }
0 commit comments