| @@ -116,7 +116,7 @@ use crate::fixture_path; · 115 unmodified lines |
| 116 | | 116 | |
| 117 | #[test] | 117 | #[test] |
| 118 | fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden() { | 118 | fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden() { |
| 119 | let mut opts = opts_with_symlink(true); | 119 | let mut opts = opts_from_probe(); |
| 120 | // without overwrite mode, everything is safe. | 120 | // without overwrite mode, everything is safe. |
| 121 | opts.overwrite_existing = false; | 121 | opts.overwrite_existing = false; |
| 122 | let (source_tree, destination, _index, outcome) = | 122 | let (source_tree, destination, _index, outcome) = |
| @@ -125,8 +125,7 @@ fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden( · 2 unmodified lines |
| 125 | let source_files = dir_structure(&source_tree); | 125 | let source_files = dir_structure(&source_tree); |
| 126 | let worktree_files = dir_structure(&destination); | 126 | let worktree_files = dir_structure(&destination); |
| 127 | | 127 | |
| 128 | let fs_caps = probe_gitoxide_dir().unwrap(); | 128 | if opts.fs.ignore_case { |
| 129 | if fs_caps.ignore_case { | | |
| 130 | assert_eq!( | 129 | assert_eq!( |
| 131 | stripped_prefix(&source_tree, &source_files), | 130 | stripped_prefix(&source_tree, &source_files), |
| 132 | stripped_prefix(&destination, &worktree_files), | 131 | stripped_prefix(&destination, &worktree_files), |
| @@ -153,9 +152,9 @@ fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden( · 20 unmodified lines |
| 153 | } | 152 | } |
| 154 | | 153 | |
| 155 | #[test] | 154 | #[test] |
| 156 | #[ignore] | 155 | #[ignore] // TODO: needs recursive directory deletion |
| 157 | fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { | 156 | fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { |
| 158 | let mut opts = opts_with_symlink(true); | 157 | let mut opts = opts_from_probe(); |
| 159 | // with overwrite mode | 158 | // with overwrite mode |
| 160 | opts.overwrite_existing = true; | 159 | opts.overwrite_existing = true; |
| 161 | let (source_tree, destination, _index, outcome) = | 160 | let (source_tree, destination, _index, outcome) = |
| @@ -164,29 +163,12 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { · 2 unmodified lines |
| 164 | let source_files = dir_structure(&source_tree); | 163 | let source_files = dir_structure(&source_tree); |
| 165 | let worktree_files = dir_structure(&destination); | 164 | let worktree_files = dir_structure(&destination); |
| 166 | | 165 | |
| 167 | let fs_caps = probe_gitoxide_dir().unwrap(); | 166 | if opts.fs.ignore_case { |
| 168 | if fs_caps.ignore_case { | | |
| 169 | assert_eq!( | 167 | assert_eq!( |
| 170 | stripped_prefix(&source_tree, &source_files), | 168 | stripped_prefix(&source_tree, &source_files), |
| 171 | paths(["A-dir/a", "A-file", "fake-dir/b", "fake-file"]) | | |
| 172 | ); | | |
| 173 | assert_eq!( | | |
| 174 | stripped_prefix(&destination, &worktree_files), | 169 | stripped_prefix(&destination, &worktree_files), |
| 175 | paths(["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE"]) | | |
| 176 | ); | | |
| 177 | assert_eq!( | | |
| 178 | outcome.collisions, | | |
| 179 | vec![ | | |
| 180 | Collision { | | |
| 181 | path: "fake-dir/b".into(), | | |
| 182 | error_kind: AlreadyExists | | |
| 183 | }, | | |
| 184 | Collision { | | |
| 185 | path: "fake-file".into(), | | |
| 186 | error_kind: AlreadyExists | | |
| 187 | } | | |
| 188 | ] | | |
| 189 | ); | 170 | ); |
| | 171 | assert!(outcome.collisions.is_empty()); |
| 190 | } else { | 172 | } else { |
| 191 | let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"]; | 173 | let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"]; |
| 192 | assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected)); | 174 | assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected)); |
| @@ -196,23 +178,69 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { · 3 unmodified lines |
| 196 | } | 178 | } |
| 197 | | 179 | |
| 198 | #[test] | 180 | #[test] |
| 199 | fn symlinks_become_files_if_disabled() -> crate::Result { | 181 | fn overwriting_files_and_lone_directories_works() { |
| 200 | let opts = opts_with_symlink(false); | 182 | let mut opts = opts_from_probe(); |
| 201 | let (source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; | 183 | opts.overwrite_existing = true; |
| | 184 | opts.destination_is_initially_empty = false; |
| | 185 | let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( |
| | 186 | opts, |
| | 187 | "make_mixed_without_submodules", |
| | 188 | |_| true, |
| | 189 | |d| { |
| | 190 | let empty = d.join("empty"); |
| | 191 | symlink::symlink_dir(d.join(".."), &empty)?; // empty is symlink to the directory above |
| | 192 | std::fs::write(d.join("executable"), b"foo")?; // executable is regular file and has different content |
| | 193 | let dir = d.join("dir"); |
| | 194 | std::fs::create_dir(&dir)?; |
| | 195 | std::fs::create_dir(dir.join("content"))?; // 'content' is a directory now |
| | 196 | |
| | 197 | let dir = dir.join("sub-dir"); |
| | 198 | std::fs::create_dir(&dir)?; |
| | 199 | |
| | 200 | symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to another file |
| | 201 | Ok(()) |
| | 202 | }, |
| | 203 | ) |
| | 204 | .unwrap(); |
| 202 | | 205 | |
| 203 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; | | |
| 204 | assert!(outcome.collisions.is_empty()); | 206 | assert!(outcome.collisions.is_empty()); |
| 205 | Ok(()) | 207 | |
| | 208 | assert_eq!( |
| | 209 | stripped_prefix(&destination, &dir_structure(&destination)), |
| | 210 | paths(["dir/content", "dir/sub-dir/symlink", "empty", "executable"]) |
| | 211 | ); |
| | 212 | let meta = std::fs::symlink_metadata(destination.path().join("empty")).unwrap(); |
| | 213 | assert!(meta.is_file(), "'empty' is now a file"); |
| | 214 | assert_eq!(meta.len(), 0, "'empty' is indeed empty"); |
| | 215 | |
| | 216 | let exe = destination.path().join("executable"); |
| | 217 | assert_eq!( |
| | 218 | std::fs::read(&exe).unwrap(), |
| | 219 | b"content", |
| | 220 | "'exe' has the correct content" |
| | 221 | ); |
| | 222 | |
| | 223 | let meta = std::fs::symlink_metadata(exe).unwrap(); |
| | 224 | assert!(meta.is_file()); |
| | 225 | if opts.fs.executable_bit { |
| | 226 | #[cfg(unix)] |
| | 227 | assert_eq!(meta.mode() & 0o700, 0o700, "the executable bit is set where supported"); |
| | 228 | } |
| | 229 | |
| | 230 | assert_eq!( |
| | 231 | std::fs::read(destination.path().join("dir/content")).unwrap(), |
| | 232 | b"other content" |
| | 233 | ); |
| | 234 | |
| | 235 | let symlink = destination.path().join("dir/sub-dir/symlink"); |
| | 236 | assert!(std::fs::symlink_metadata(&symlink).unwrap().is_symlink(),); |
| | 237 | assert_eq!(std::fs::read(symlink).unwrap(), b"other content"); |
| 206 | } | 238 | } |
| 207 | | 239 | |
| 208 | #[test] | 240 | #[test] |
| 209 | fn allow_symlinks() -> crate::Result { | 241 | fn symlinks_become_files_if_disabled() -> crate::Result { |
| 210 | let opts = opts_with_symlink(true); | 242 | let mut opts = opts_from_probe(); |
| 211 | if !probe_gitoxide_dir()?.symlink { | 243 | opts.fs.symlink = false; |
| 212 | eprintln!("IGNORING symlink test on file system without symlink support"); | | |
| 213 | // skip if symlinks aren't supported anyway. | | |
| 214 | return Ok(()); | | |
| 215 | }; | | |
| 216 | let (source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; | 244 | let (source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; |
| 217 | | 245 | |
| 218 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; | 246 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; |
| @@ -220,21 +248,39 @@ fn allow_symlinks() -> crate::Result { · 1 unmodified line |
| 220 | Ok(()) | 248 | Ok(()) |
| 221 | } | 249 | } |
| 222 | | 250 | |
| | 251 | #[test] |
| | 252 | fn allow_or_disallow_symlinks() -> crate::Result { |
| | 253 | let mut opts = opts_from_probe(); |
| | 254 | for allowed in &[false, true] { |
| | 255 | opts.fs.symlink = *allowed; |
| | 256 | let (source_tree, destination, _index, outcome) = |
| | 257 | checkout_index_in_tmp_dir(opts, "make_mixed_without_submodules")?; |
| | 258 | |
| | 259 | assert_equality(&source_tree, &destination, opts.fs.symlink)?; |
| | 260 | assert!(outcome.collisions.is_empty()); |
| | 261 | } |
| | 262 | Ok(()) |
| | 263 | } |
| | 264 | |
| 223 | #[test] | 265 | #[test] |
| 224 | fn keep_going_collects_results() { | 266 | fn keep_going_collects_results() { |
| 225 | let mut opts = opts_with_symlink(true); | 267 | let mut opts = opts_from_probe(); |
| 226 | opts.keep_going = true; | 268 | opts.keep_going = true; |
| 227 | let mut count = 0; | 269 | let mut count = 0; |
| 228 | let (_source_tree, destination, _index, outcome) = | 270 | let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( |
| 229 | checkout_index_in_tmp_dir_prep_dest(opts, "make_mixed_without_submodules", |_id| { | 271 | opts, |
| | 272 | "make_mixed_without_submodules", |
| | 273 | |_id| { |
| 230 | if count < 2 { | 274 | if count < 2 { |
| 231 | count += 1; | 275 | count += 1; |
| 232 | false | 276 | false |
| 233 | } else { | 277 | } else { |
| 234 | true | 278 | true |
| 235 | } | 279 | } |
| 236 | }) | 280 | }, |
| 237 | .unwrap(); | 281 | |_| Ok(()), |
| | 282 | ) |
| | 283 | .unwrap(); |
| 238 | | 284 | |
| 239 | assert_eq!( | 285 | assert_eq!( |
| 240 | stripped_prefix(&destination, &dir_structure(&destination)), | 286 | stripped_prefix(&destination, &dir_structure(&destination)), |
| @@ -255,13 +301,11 @@ fn keep_going_collects_results() { · 14 unmodified lines |
| 255 | | 301 | |
| 256 | #[test] | 302 | #[test] |
| 257 | fn no_case_related_collisions_on_case_sensitive_filesystem() { | 303 | fn no_case_related_collisions_on_case_sensitive_filesystem() { |
| 258 | let fs_caps = probe_gitoxide_dir().unwrap(); | 304 | let opts = opts_from_probe(); |
| 259 | if fs_caps.ignore_case { | 305 | if opts.fs.ignore_case { |
| 260 | eprintln!("Skipping case-sensitive testing on what would be a case-insensitive file system"); | 306 | eprintln!("Skipping case-sensitive testing on what would be a case-insensitive file system"); |
| 261 | return; | 307 | return; |
| 262 | } | 308 | } |
| 263 | let mut opts = opts_with_symlink(true); | | |
| 264 | opts.fs = fs_caps; | | |
| 265 | let (source_tree, destination, index, outcome) = | 309 | let (source_tree, destination, index, outcome) = |
| 266 | checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); | 310 | checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); |
| 267 | | 311 | |
| @@ -272,12 +316,11 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { · 4 unmodified lines |
| 272 | | 316 | |
| 273 | #[test] | 317 | #[test] |
| 274 | fn collisions_are_detected_on_a_case_sensitive_filesystem() { | 318 | fn collisions_are_detected_on_a_case_sensitive_filesystem() { |
| 275 | let fs_caps = probe_gitoxide_dir().unwrap(); | 319 | let opts = opts_from_probe(); |
| 276 | if !fs_caps.ignore_case { | 320 | if !opts.fs.ignore_case { |
| 277 | eprintln!("Skipping case-insensitive testing on what would be a case-senstive file system"); | 321 | eprintln!("Skipping case-insensitive testing on what would be a case-senstive file system"); |
| 278 | return; | 322 | return; |
| 279 | } | 323 | } |
| 280 | let opts = opts_with_symlink(fs_caps.symlink); | | |
| 281 | let (source_tree, destination, _index, outcome) = | 324 | let (source_tree, destination, _index, outcome) = |
| 282 | checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); | 325 | checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap(); |
| 283 | | 326 | |
| @@ -379,13 +422,14 @@ fn checkout_index_in_tmp_dir( · 95 unmodified lines |
| 379 | git_index::File, | 422 | git_index::File, |
| 380 | git_worktree::index::checkout::Outcome, | 423 | git_worktree::index::checkout::Outcome, |
| 381 | )> { | 424 | )> { |
| 382 | checkout_index_in_tmp_dir_prep_dest(opts, name, |_d| true) | 425 | checkout_index_in_tmp_dir_opts(opts, name, |_d| true, |_| Ok(())) |
| 383 | } | 426 | } |
| 384 | | 427 | |
| 385 | fn checkout_index_in_tmp_dir_prep_dest( | 428 | fn checkout_index_in_tmp_dir_opts( |
| 386 | opts: index::checkout::Options, | 429 | opts: index::checkout::Options, |
| 387 | name: &str, | 430 | name: &str, |
| 388 | mut allow_return_object: impl FnMut(&git_hash::oid) -> bool, | 431 | mut allow_return_object: impl FnMut(&git_hash::oid) -> bool, |
| | 432 | prep_dest: impl Fn(&Path) -> std::io::Result<()>, |
| 389 | ) -> crate::Result<( | 433 | ) -> crate::Result<( |
| 390 | PathBuf, | 434 | PathBuf, |
| 391 | TempDir, | 435 | TempDir, |
| @@ -397,6 +441,7 @@ fn checkout_index_in_tmp_dir_prep_dest( · 5 unmodified lines |
| 397 | let mut index = git_index::File::at(git_dir.join("index"), Default::default())?; | 441 | let mut index = git_index::File::at(git_dir.join("index"), Default::default())?; |
| 398 | let odb = git_odb::at(git_dir.join("objects"))?; | 442 | let odb = git_odb::at(git_dir.join("objects"))?; |
| 399 | let destination = tempfile::tempdir_in(std::env::current_dir()?)?; | 443 | let destination = tempfile::tempdir_in(std::env::current_dir()?)?; |
| | 444 | prep_dest(destination.path())?; |
| 400 | | 445 | |
| 401 | let outcome = index::checkout( | 446 | let outcome = index::checkout( |
| 402 | &mut index, | 447 | &mut index, |
| @@ -425,12 +470,9 @@ fn probe_gitoxide_dir() -> crate::Result<Capabilities> { · 22 unmodified lines |
| 425 | )) | 470 | )) |
| 426 | } | 471 | } |
| 427 | | 472 | |
| 428 | fn opts_with_symlink(symlink: bool) -> index::checkout::Options { | 473 | fn opts_from_probe() -> index::checkout::Options { |
| 429 | index::checkout::Options { | 474 | index::checkout::Options { |
| 430 | fs: git_worktree::fs::Capabilities { | 475 | fs: probe_gitoxide_dir().unwrap(), |
| 431 | symlink, | | |
| 432 | ..Default::default() | | |
| 433 | }, | | |
| 434 | destination_is_initially_empty: true, | 476 | destination_is_initially_empty: true, |
| 435 | ..Default::default() | 477 | ..Default::default() |
| 436 | } | 478 | } |