Unified · Split
| @@ -1,245 +0,0 @@ | |||
| 1 | use git_hash::oid; | ||
| 2 | |||
| 3 | use crate::{index, index::checkout::Collision}; | ||
| 4 | |||
| 5 | pub mod checkout { | ||
| 6 | use bstr::BString; | ||
| 7 | use quick_error::quick_error; | ||
| 8 | |||
| 9 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | ||
| 10 | pub struct Collision { | ||
| 11 | /// the path that collided with something already present on disk. | ||
| 12 | pub path: BString, | ||
| 13 | /// The io error we encountered when checking out `path`. | ||
| 14 | pub error_kind: std::io::ErrorKind, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub struct Outcome { | ||
| 18 | pub collisions: Vec<Collision>, | ||
| 19 | } | ||
| 20 | |||
| 21 | #[derive(Clone, Copy)] | ||
| 22 | pub struct Options { | ||
| 23 | /// capabilities of the file system | ||
| 24 | pub fs: crate::fs::Capabilities, | ||
| 25 | /// If true, we assume no file to exist in the target directory, and want exclusive access to it. | ||
| 26 | /// This should be enabled when cloning to avoid checks for freshness of files. This also enables | ||
| 27 | /// detection of collisions based on whether or not exclusive file creation succeeds or fails. | ||
| 28 | pub destination_is_initially_empty: bool, | ||
| 29 | /// If true, default false, try to checkout as much as possible and don't abort on first error which isn't | ||
| 30 | /// due to a conflict. | ||
| 31 | /// The operation will never fail, but count the encountered errors instead along with their paths. | ||
| 32 | pub keep_going: bool, | ||
| 33 | /// If true, a files creation time is taken into consideration when checking if a file changed. | ||
| 34 | /// Can be set to false in case other tools alter the creation time in ways that interfere with our operation. | ||
| 35 | /// | ||
| 36 | /// Default true. | ||
| 37 | pub trust_ctime: bool, | ||
| 38 | /// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise | ||
| 39 | /// nano-second parts of mtime and ctime,uid, gid, inode and device number won't be used, leaving only | ||
| 40 | /// the whole-second part of ctime and mtime and the file size to be checked. | ||
| 41 | /// | ||
| 42 | /// Default true. | ||
| 43 | pub check_stat: bool, | ||
| 44 | } | ||
| 45 | |||
| 46 | impl Default for Options { | ||
| 47 | fn default() -> Self { | ||
| 48 | Options { | ||
| 49 | fs: Default::default(), | ||
| 50 | destination_is_initially_empty: false, | ||
| 51 | keep_going: false, | ||
| 52 | trust_ctime: true, | ||
| 53 | check_stat: true, | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | quick_error! { | ||
| 59 | #[derive(Debug)] | ||
| 60 | pub enum Error { | ||
| 61 | IllformedUtf8{ path: BString } { | ||
| 62 | display("Could not convert path to UTF8: {}", path) | ||
| 63 | } | ||
| 64 | Time(err: std::time::SystemTimeError) { | ||
| 65 | from() | ||
| 66 | source(err) | ||
| 67 | display("The clock was off when reading file related metadata after updating a file on disk") | ||
| 68 | } | ||
| 69 | Io(err: std::io::Error) { | ||
| 70 | from() | ||
| 71 | source(err) | ||
| 72 | display("IO error while writing blob or reading file metadata or changing filetype") | ||
| 73 | } | ||
| 74 | ObjectNotFound{ oid: git_hash::ObjectId, path: std::path::PathBuf } { | ||
| 75 | display("object {} for checkout at {} not found in object database", oid.to_hex(), path.display()) | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | pub fn checkout<Find>( | ||
| 82 | index: &mut git_index::State, | ||
| 83 | path: impl AsRef<std::path::Path>, | ||
| 84 | mut find: Find, | ||
| 85 | options: checkout::Options, | ||
| 86 | ) -> Result<checkout::Outcome, checkout::Error> | ||
| 87 | where | ||
| 88 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Option<git_object::BlobRef<'a>>, | ||
| 89 | { | ||
| 90 | use std::io::ErrorKind::AlreadyExists; | ||
| 91 | let root = path.as_ref(); | ||
| 92 | let mut buf = Vec::new(); | ||
| 93 | let mut collisions = Vec::new(); | ||
| 94 | for (entry, entry_path) in index.entries_mut_with_paths() { | ||
| 95 | // TODO: write test for that | ||
| 96 | if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { | ||
| 97 | continue; | ||
| 98 | } | ||
| 99 | |||
| 100 | let res = entry::checkout(entry, entry_path, &mut find, root, options, &mut buf); | ||
| 101 | match res { | ||
| 102 | Ok(()) => {} | ||
| 103 | // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error() | ||
| 104 | #[cfg(windows)] | ||
| 105 | Err(index::checkout::Error::Io(err)) | ||
| 106 | if err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied => | ||
| 107 | { | ||
| 108 | collisions.push(Collision { | ||
| 109 | path: entry_path.into(), | ||
| 110 | error_kind: err.kind(), | ||
| 111 | }); | ||
| 112 | } | ||
| 113 | #[cfg(not(windows))] | ||
| 114 | Err(index::checkout::Error::Io(err)) if err.kind() == AlreadyExists || err.raw_os_error() == Some(21) => { | ||
| 115 | // We are here because a file existed or was blocked by a directory which shouldn't be possible unless | ||
| 116 | // we are on a file insensitive file system. | ||
| 117 | collisions.push(Collision { | ||
| 118 | path: entry_path.into(), | ||
| 119 | error_kind: err.kind(), | ||
| 120 | }); | ||
| 121 | } | ||
| 122 | Err(err) => { | ||
| 123 | if options.keep_going { | ||
| 124 | todo!("keep going") | ||
| 125 | } else { | ||
| 126 | return Err(err); | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | Ok(checkout::Outcome { collisions }) | ||
| 132 | } | ||
| 133 | |||
| 134 | pub(crate) mod entry { | ||
| 135 | use std::{ | ||
| 136 | convert::TryInto, | ||
| 137 | fs::{create_dir_all, OpenOptions}, | ||
| 138 | io::Write, | ||
| 139 | time::Duration, | ||
| 140 | }; | ||
| 141 | |||
| 142 | use bstr::BStr; | ||
| 143 | use git_hash::oid; | ||
| 144 | use git_index::Entry; | ||
| 145 | |||
| 146 | use crate::index; | ||
| 147 | |||
| 148 | #[cfg_attr(not(unix), allow(unused_variables))] | ||
| 149 | pub fn checkout<Find>( | ||
| 150 | entry: &mut Entry, | ||
| 151 | entry_path: &BStr, | ||
| 152 | find: &mut Find, | ||
| 153 | root: &std::path::Path, | ||
| 154 | index::checkout::Options { | ||
| 155 | fs: | ||
| 156 | crate::fs::Capabilities { | ||
| 157 | symlink, | ||
| 158 | executable_bit, | ||
| 159 | .. | ||
| 160 | }, | ||
| 161 | destination_is_initially_empty, | ||
| 162 | .. | ||
| 163 | }: index::checkout::Options, | ||
| 164 | buf: &mut Vec<u8>, | ||
| 165 | ) -> Result<(), index::checkout::Error> | ||
| 166 | where | ||
| 167 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Option<git_object::BlobRef<'a>>, | ||
| 168 | { | ||
| 169 | let dest = root.join(git_features::path::from_byte_slice(entry_path).map_err(|_| { | ||
| 170 | index::checkout::Error::IllformedUtf8 { | ||
| 171 | path: entry_path.to_owned(), | ||
| 172 | } | ||
| 173 | })?); | ||
| 174 | create_dir_all(dest.parent().expect("entry paths are never empty"))?; // TODO: can this be avoided to create dirs when needed only? | ||
| 175 | |||
| 176 | match entry.mode { | ||
| 177 | git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => { | ||
| 178 | let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { | ||
| 179 | oid: entry.id, | ||
| 180 | path: root.to_path_buf(), | ||
| 181 | })?; | ||
| 182 | let mut options = OpenOptions::new(); | ||
| 183 | options | ||
| 184 | .create_new(destination_is_initially_empty) | ||
| 185 | .create(!destination_is_initially_empty) | ||
| 186 | .write(true); | ||
| 187 | #[cfg(unix)] | ||
| 188 | if executable_bit && entry.mode == git_index::entry::Mode::FILE_EXECUTABLE { | ||
| 189 | use std::os::unix::fs::OpenOptionsExt; | ||
| 190 | options.mode(0o777); | ||
| 191 | } | ||
| 192 | |||
| 193 | let mut file = options.open(&dest)?; | ||
| 194 | file.write_all(obj.data)?; | ||
| 195 | // NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well. | ||
| 196 | // revisit this once there is a bug to fix. | ||
| 197 | update_fstat(entry, file.metadata()?)?; | ||
| 198 | } | ||
| 199 | git_index::entry::Mode::SYMLINK => { | ||
| 200 | let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { | ||
| 201 | oid: entry.id, | ||
| 202 | path: root.to_path_buf(), | ||
| 203 | })?; | ||
| 204 | let symlink_destination = git_features::path::from_byte_slice(obj.data) | ||
| 205 | .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?; | ||
| 206 | |||
| 207 | // TODO: how to deal with mode changes? Maybe this info can be passed once we check for whether | ||
| 208 | // a checkout is needed at all. | ||
| 209 | if symlink { | ||
| 210 | symlink::symlink_auto(symlink_destination, &dest)?; | ||
| 211 | } else { | ||
| 212 | std::fs::write(&dest, obj.data)?; | ||
| 213 | } | ||
| 214 | |||
| 215 | update_fstat(entry, std::fs::symlink_metadata(&dest)?)?; | ||
| 216 | } | ||
| 217 | git_index::entry::Mode::DIR => todo!(), | ||
| 218 | git_index::entry::Mode::COMMIT => todo!(), | ||
| 219 | _ => unreachable!(), | ||
| 220 | } | ||
| 221 | Ok(()) | ||
| 222 | } | ||
| 223 | |||
| 224 | fn update_fstat(entry: &mut Entry, meta: std::fs::Metadata) -> Result<(), index::checkout::Error> { | ||
| 225 | let ctime = meta | ||
| 226 | .created() | ||
| 227 | .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; | ||
| 228 | let mtime = meta | ||
| 229 | .modified() | ||
| 230 | .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; | ||
| 231 | |||
| 232 | let stat = &mut entry.stat; | ||
| 233 | stat.mtime.secs = mtime | ||
| 234 | .as_secs() | ||
| 235 | .try_into() | ||
| 236 | .expect("by 2038 we found a solution for this"); | ||
| 237 | stat.mtime.nsecs = mtime.subsec_nanos(); | ||
| 238 | stat.ctime.secs = ctime | ||
| 239 | .as_secs() | ||
| 240 | .try_into() | ||
| 241 | .expect("by 2038 we found a solution for this"); | ||
| 242 | stat.ctime.nsecs = ctime.subsec_nanos(); | ||
| 243 | Ok(()) | ||
| 244 | } | ||
| 245 | } | ||
| @@ -0,0 +1,73 @@ | |||
| 1 | use bstr::BString; | ||
| 2 | use quick_error::quick_error; | ||
| 3 | |||
| 4 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | ||
| 5 | pub struct Collision { | ||
| 6 | /// the path that collided with something already present on disk. | ||
| 7 | pub path: BString, | ||
| 8 | /// The io error we encountered when checking out `path`. | ||
| 9 | pub error_kind: std::io::ErrorKind, | ||
| 10 | } | ||
| 11 | |||
| 12 | pub struct Outcome { | ||
| 13 | pub collisions: Vec<Collision>, | ||
| 14 | } | ||
| 15 | |||
| 16 | #[derive(Clone, Copy)] | ||
| 17 | pub struct Options { | ||
| 18 | /// capabilities of the file system | ||
| 19 | pub fs: crate::fs::Capabilities, | ||
| 20 | /// If true, we assume no file to exist in the target directory, and want exclusive access to it. | ||
| 21 | /// This should be enabled when cloning to avoid checks for freshness of files. This also enables | ||
| 22 | /// detection of collisions based on whether or not exclusive file creation succeeds or fails. | ||
| 23 | pub destination_is_initially_empty: bool, | ||
| 24 | /// If true, default false, try to checkout as much as possible and don't abort on first error which isn't | ||
| 25 | /// due to a conflict. | ||
| 26 | /// The operation will never fail, but count the encountered errors instead along with their paths. | ||
| 27 | pub keep_going: bool, | ||
| 28 | /// If true, a files creation time is taken into consideration when checking if a file changed. | ||
| 29 | /// Can be set to false in case other tools alter the creation time in ways that interfere with our operation. | ||
| 30 | /// | ||
| 31 | /// Default true. | ||
| 32 | pub trust_ctime: bool, | ||
| 33 | /// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise | ||
| 34 | /// nano-second parts of mtime and ctime,uid, gid, inode and device number won't be used, leaving only | ||
| 35 | /// the whole-second part of ctime and mtime and the file size to be checked. | ||
| 36 | /// | ||
| 37 | /// Default true. | ||
| 38 | pub check_stat: bool, | ||
| 39 | } | ||
| 40 | |||
| 41 | impl Default for Options { | ||
| 42 | fn default() -> Self { | ||
| 43 | Options { | ||
| 44 | fs: Default::default(), | ||
| 45 | destination_is_initially_empty: false, | ||
| 46 | keep_going: false, | ||
| 47 | trust_ctime: true, | ||
| 48 | check_stat: true, | ||
| 49 | } | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | quick_error! { | ||
| 54 | #[derive(Debug)] | ||
| 55 | pub enum Error { | ||
| 56 | IllformedUtf8{ path: BString } { | ||
| 57 | display("Could not convert path to UTF8: {}", path) | ||
| 58 | } | ||
| 59 | Time(err: std::time::SystemTimeError) { | ||
| 60 | from() | ||
| 61 | source(err) | ||
| 62 | display("The clock was off when reading file related metadata after updating a file on disk") | ||
| 63 | } | ||
| 64 | Io(err: std::io::Error) { | ||
| 65 | from() | ||
| 66 | source(err) | ||
| 67 | display("IO error while writing blob or reading file metadata or changing filetype") | ||
| 68 | } | ||
| 69 | ObjectNotFound{ oid: git_hash::ObjectId, path: std::path::PathBuf } { | ||
| 70 | display("object {} for checkout at {} not found in object database", oid.to_hex(), path.display()) | ||
| 71 | } | ||
| 72 | } | ||
| 73 | } | ||
| @@ -0,0 +1,109 @@ | |||
| 1 | use std::{ | ||
| 2 | convert::TryInto, | ||
| 3 | fs::{create_dir_all, OpenOptions}, | ||
| 4 | io::Write, | ||
| 5 | time::Duration, | ||
| 6 | }; | ||
| 7 | |||
| 8 | use bstr::BStr; | ||
| 9 | use git_hash::oid; | ||
| 10 | use git_index::Entry; | ||
| 11 | |||
| 12 | use crate::index; | ||
| 13 | |||
| 14 | #[cfg_attr(not(unix), allow(unused_variables))] | ||
| 15 | pub fn checkout<Find>( | ||
| 16 | entry: &mut Entry, | ||
| 17 | entry_path: &BStr, | ||
| 18 | find: &mut Find, | ||
| 19 | root: &std::path::Path, | ||
| 20 | index::checkout::Options { | ||
| 21 | fs: crate::fs::Capabilities { | ||
| 22 | symlink, | ||
| 23 | executable_bit, | ||
| 24 | .. | ||
| 25 | }, | ||
| 26 | destination_is_initially_empty, | ||
| 27 | .. | ||
| 28 | }: index::checkout::Options, | ||
| 29 | buf: &mut Vec<u8>, | ||
| 30 | ) -> Result<(), index::checkout::Error> | ||
| 31 | where | ||
| 32 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Option<git_object::BlobRef<'a>>, | ||
| 33 | { | ||
| 34 | let dest = root.join(git_features::path::from_byte_slice(entry_path).map_err(|_| { | ||
| 35 | index::checkout::Error::IllformedUtf8 { | ||
| 36 | path: entry_path.to_owned(), | ||
| 37 | } | ||
| 38 | })?); | ||
| 39 | create_dir_all(dest.parent().expect("entry paths are never empty"))?; // TODO: can this be avoided to create dirs when needed only? | ||
| 40 | |||
| 41 | match entry.mode { | ||
| 42 | git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => { | ||
| 43 | let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { | ||
| 44 | oid: entry.id, | ||
| 45 | path: root.to_path_buf(), | ||
| 46 | })?; | ||
| 47 | let mut options = OpenOptions::new(); | ||
| 48 | options | ||
| 49 | .create_new(destination_is_initially_empty) | ||
| 50 | .create(!destination_is_initially_empty) | ||
| 51 | .write(true); | ||
| 52 | #[cfg(unix)] | ||
| 53 | if executable_bit && entry.mode == git_index::entry::Mode::FILE_EXECUTABLE { | ||
| 54 | use std::os::unix::fs::OpenOptionsExt; | ||
| 55 | options.mode(0o777); | ||
| 56 | } | ||
| 57 | |||
| 58 | let mut file = options.open(&dest)?; | ||
| 59 | file.write_all(obj.data)?; | ||
| 60 | // NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well. | ||
| 61 | // revisit this once there is a bug to fix. | ||
| 62 | update_fstat(entry, file.metadata()?)?; | ||
| 63 | } | ||
| 64 | git_index::entry::Mode::SYMLINK => { | ||
| 65 | let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { | ||
| 66 | oid: entry.id, | ||
| 67 | path: root.to_path_buf(), | ||
| 68 | })?; | ||
| 69 | let symlink_destination = git_features::path::from_byte_slice(obj.data) | ||
| 70 | .map_err(|_| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?; | ||
| 71 | |||
| 72 | // TODO: how to deal with mode changes? Maybe this info can be passed once we check for whether | ||
| 73 | // a checkout is needed at all. | ||
| 74 | if symlink { | ||
| 75 | symlink::symlink_auto(symlink_destination, &dest)?; | ||
| 76 | } else { | ||
| 77 | std::fs::write(&dest, obj.data)?; | ||
| 78 | } | ||
| 79 | |||
| 80 | update_fstat(entry, std::fs::symlink_metadata(&dest)?)?; | ||
| 81 | } | ||
| 82 | git_index::entry::Mode::DIR => todo!(), | ||
| 83 | git_index::entry::Mode::COMMIT => todo!(), | ||
| 84 | _ => unreachable!(), | ||
| 85 | } | ||
| 86 | Ok(()) | ||
| 87 | } | ||
| 88 | |||
| 89 | fn update_fstat(entry: &mut Entry, meta: std::fs::Metadata) -> Result<(), index::checkout::Error> { | ||
| 90 | let ctime = meta | ||
| 91 | .created() | ||
| 92 | .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; | ||
| 93 | let mtime = meta | ||
| 94 | .modified() | ||
| 95 | .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; | ||
| 96 | |||
| 97 | let stat = &mut entry.stat; | ||
| 98 | stat.mtime.secs = mtime | ||
| 99 | .as_secs() | ||
| 100 | .try_into() | ||
| 101 | .expect("by 2038 we found a solution for this"); | ||
| 102 | stat.mtime.nsecs = mtime.subsec_nanos(); | ||
| 103 | stat.ctime.secs = ctime | ||
| 104 | .as_secs() | ||
| 105 | .try_into() | ||
| 106 | .expect("by 2038 we found a solution for this"); | ||
| 107 | stat.ctime.nsecs = ctime.subsec_nanos(); | ||
| 108 | Ok(()) | ||
| 109 | } | ||
| @@ -0,0 +1,63 @@ | |||
| 1 | use git_hash::oid; | ||
| 2 | |||
| 3 | use crate::{index, index::checkout::Collision}; | ||
| 4 | |||
| 5 | pub mod checkout; | ||
| 6 | pub(crate) mod entry; | ||
| 7 | |||
| 8 | pub fn checkout<Find>( | ||
| 9 | index: &mut git_index::State, | ||
| 10 | path: impl AsRef<std::path::Path>, | ||
| 11 | mut find: Find, | ||
| 12 | options: checkout::Options, | ||
| 13 | ) -> Result<checkout::Outcome, checkout::Error> | ||
| 14 | where | ||
| 15 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Option<git_object::BlobRef<'a>>, | ||
| 16 | { | ||
| 17 | if !options.destination_is_initially_empty { | ||
| 18 | todo!("deal with non-clone checkouts") | ||
| 19 | } | ||
| 20 | |||
| 21 | use std::io::ErrorKind::AlreadyExists; | ||
| 22 | let root = path.as_ref(); | ||
| 23 | let mut buf = Vec::new(); | ||
| 24 | let mut collisions = Vec::new(); | ||
| 25 | for (entry, entry_path) in index.entries_mut_with_paths() { | ||
| 26 | // TODO: write test for that | ||
| 27 | if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { | ||
| 28 | continue; | ||
| 29 | } | ||
| 30 | |||
| 31 | let res = entry::checkout(entry, entry_path, &mut find, root, options, &mut buf); | ||
| 32 | match res { | ||
| 33 | Ok(()) => {} | ||
| 34 | // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error() | ||
| 35 | #[cfg(windows)] | ||
| 36 | Err(index::checkout::Error::Io(err)) | ||
| 37 | if err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied => | ||
| 38 | { | ||
| 39 | collisions.push(Collision { | ||
| 40 | path: entry_path.into(), | ||
| 41 | error_kind: err.kind(), | ||
| 42 | }); | ||
| 43 | } | ||
| 44 | #[cfg(not(windows))] | ||
| 45 | Err(index::checkout::Error::Io(err)) if err.kind() == AlreadyExists || err.raw_os_error() == Some(21) => { | ||
| 46 | // We are here because a file existed or was blocked by a directory which shouldn't be possible unless | ||
| 47 | // we are on a file insensitive file system. | ||
| 48 | collisions.push(Collision { | ||
| 49 | path: entry_path.into(), | ||
| 50 | error_kind: err.kind(), | ||
| 51 | }); | ||
| 52 | } | ||
| 53 | Err(err) => { | ||
| 54 | if options.keep_going { | ||
| 55 | todo!("keep going") | ||
| 56 | } else { | ||
| 57 | return Err(err); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | Ok(checkout::Outcome { collisions }) | ||
| 63 | } | ||
| @@ -14,27 +14,6 @@ mod checkout { · 13 unmodified lines | |||
| 14 | 14 | ||
| 15 | use crate::fixture_path; | 15 | use crate::fixture_path; |
| 16 | 16 | ||
| 17 | fn probe_gitoxide_dir() -> crate::Result<Capabilities> { | ||
| 18 | Ok(git_worktree::fs::Capabilities::probe( | ||
| 19 | std::env::current_dir()?.join("..").join(".git"), | ||
| 20 | )) | ||
| 21 | } | ||
| 22 | |||
| 23 | fn opts_with_symlink(symlink: bool) -> index::checkout::Options { | ||
| 24 | index::checkout::Options { | ||
| 25 | fs: git_worktree::fs::Capabilities { | ||
| 26 | symlink, | ||
| 27 | ..Default::default() | ||
| 28 | }, | ||
| 29 | destination_is_initially_empty: true, | ||
| 30 | ..Default::default() | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | fn paths<'a>(p: impl IntoIterator<Item = &'a str>) -> Vec<PathBuf> { | ||
| 35 | p.into_iter().map(PathBuf::from).collect() | ||
| 36 | } | ||
| 37 | |||
| 38 | #[test] | 17 | #[test] |
| 39 | fn symlinks_become_files_if_disabled() -> crate::Result { | 18 | fn symlinks_become_files_if_disabled() -> crate::Result { |
| 40 | let opts = opts_with_symlink(false); | 19 | let opts = opts_with_symlink(false); |
| @@ -205,4 +184,25 @@ mod checkout { · 164 unmodified lines | |||
| 205 | fn stripped_prefix(prefix: impl AsRef<Path>, source_files: &[PathBuf]) -> Vec<&Path> { | 184 | fn stripped_prefix(prefix: impl AsRef<Path>, source_files: &[PathBuf]) -> Vec<&Path> { |
| 206 | source_files.iter().flat_map(|p| p.strip_prefix(&prefix)).collect() | 185 | source_files.iter().flat_map(|p| p.strip_prefix(&prefix)).collect() |
| 207 | } | 186 | } |
| 187 | |||
| 188 | fn probe_gitoxide_dir() -> crate::Result<Capabilities> { | ||
| 189 | Ok(git_worktree::fs::Capabilities::probe( | ||
| 190 | std::env::current_dir()?.join("..").join(".git"), | ||
| 191 | )) | ||
| 192 | } | ||
| 193 | |||
| 194 | fn opts_with_symlink(symlink: bool) -> index::checkout::Options { | ||
| 195 | index::checkout::Options { | ||
| 196 | fs: git_worktree::fs::Capabilities { | ||
| 197 | symlink, | ||
| 198 | ..Default::default() | ||
| 199 | }, | ||
| 200 | destination_is_initially_empty: true, | ||
| 201 | ..Default::default() | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | fn paths<'a>(p: impl IntoIterator<Item = &'a str>) -> Vec<PathBuf> { | ||
| 206 | p.into_iter().map(PathBuf::from).collect() | ||
| 207 | } | ||
| 208 | } | 208 | } |