#![forbid(unsafe_code, rust_2018_idioms)] pub mod index { use git_hash::oid; pub mod checkout { use bstr::BString; use quick_error::quick_error; #[derive(Clone, Copy)] pub struct Options { pub symlinks: bool, } impl Default for Options { fn default() -> Self { Options { symlinks: true } } } quick_error! { #[derive(Debug)] pub enum Error { IllformedUtf8{ path: BString } { display("Could not convert path to UTF8: {}", path) } Time(err: std::time::SystemTimeError) { from() source(err) display("The clock was off when reading file related metadata after updating a file on disk") } Io(err: std::io::Error) { from() source(err) display("IO error while writing blob or reading file metadata or changing filetype") } ObjectNotFound{ oid: git_hash::ObjectId, path: std::path::PathBuf } { display("object {} for checkout at {} not found in object database", oid.to_hex(), path.display()) } } } } pub fn checkout( index: &mut git_index::State, path: impl AsRef, mut find: Find, options: checkout::Options, ) -> Result<(), checkout::Error> where Find: for<'a> FnMut(&oid, &'a mut Vec) -> Option>, { let root = path.as_ref(); let mut buf = Vec::new(); for (entry, entry_path) in index.entries_mut_with_paths() { // TODO: write test for that if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) { continue; } entry::checkout(entry, entry_path, &mut find, root, options, &mut buf)?; } Ok(()) } pub(crate) mod entry { use std::{ convert::TryInto, fs::{create_dir_all, OpenOptions}, io::Write, time::Duration, }; use bstr::BStr; use git_hash::oid; use git_index::Entry; use crate::index; pub fn checkout( entry: &mut Entry, entry_path: &BStr, find: &mut Find, root: &std::path::Path, index::checkout::Options { symlinks }: index::checkout::Options, buf: &mut Vec, ) -> Result<(), index::checkout::Error> where Find: for<'a> FnMut(&oid, &'a mut Vec) -> Option>, { let dest = root.join(git_features::path::from_byte_slice(entry_path).ok_or_else(|| { index::checkout::Error::IllformedUtf8 { path: entry_path.to_owned(), } })?); create_dir_all(dest.parent().expect("entry paths are never empty"))?; // TODO: can this be avoided to create dirs when needed only? match entry.mode { git_index::entry::Mode::FILE | git_index::entry::Mode::FILE_EXECUTABLE => { let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { oid: entry.id, path: root.to_path_buf(), })?; let mut options = OpenOptions::new(); options.write(true).create_new(true); #[cfg(unix)] if entry.mode == git_index::entry::Mode::FILE_EXECUTABLE { use std::os::unix::fs::OpenOptionsExt; options.mode(0o777); } let mut file = options.open(&dest)?; file.write_all(obj.data)?; let met = file.metadata()?; let ctime = met .created() .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; let mtime = met .modified() .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; update_fstat(entry, ctime, mtime)?; } git_index::entry::Mode::SYMLINK => { let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound { oid: entry.id, path: root.to_path_buf(), })?; let symlink_destination = git_features::path::from_byte_slice(obj.data) .ok_or_else(|| index::checkout::Error::IllformedUtf8 { path: obj.data.into() })?; if symlinks { #[cfg(unix)] std::os::unix::fs::symlink(symlink_destination, &dest)?; #[cfg(windows)] if dest.exists() { if dest.is_file() { std::os::windows::fs::symlink_file(symlink_destination, &dest)?; } else { std::os::windows::fs::symlink_dir(symlink_destination, &dest)?; } } } else { std::fs::write(&dest, obj.data)?; } let met = std::fs::symlink_metadata(&dest)?; let ctime = met .created() .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; let mtime = met .modified() .map_or(Ok(Duration::default()), |x| x.duration_since(std::time::UNIX_EPOCH))?; update_fstat(entry, ctime, mtime)?; } git_index::entry::Mode::DIR => todo!(), git_index::entry::Mode::COMMIT => todo!(), _ => unreachable!(), } Ok(()) } fn update_fstat(entry: &mut Entry, ctime: Duration, mtime: Duration) -> Result<(), index::checkout::Error> { let stat = &mut entry.stat; stat.mtime.secs = mtime .as_secs() .try_into() .expect("by 2038 we found a solution for this"); stat.mtime.nsecs = mtime.subsec_nanos(); stat.ctime.secs = ctime .as_secs() .try_into() .expect("by 2038 we found a solution for this"); stat.ctime.nsecs = ctime.subsec_nanos(); Ok(()) } } }