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 | 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 | 17 | #[test] |
| 39 | 18 | fn symlinks_become_files_if_disabled() -> crate::Result { |
| 40 | 19 | let opts = opts_with_symlink(false); |
| @@ -205,4 +184,25 @@ mod checkout { · 164 unmodified lines | ||
| 205 | 184 | fn stripped_prefix(prefix: impl AsRef<Path>, source_files: &[PathBuf]) -> Vec<&Path> { |
| 206 | 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 | } |