History

refactor (#301)

Sebastian Thiel committed 4 years ago

Commit 48dc4011 parent 322c3165 files · +266 −266Browse files

Showing the changes to git-worktree/src (4 of 5 files). Show all

Unified · Split

4 files
  1. git-worktree/src/index.rs +0 −245
  2. git-worktree/src/index/checkout.rs +73 −0
  3. git-worktree/src/index/entry.rs +109 −0
  4. git-worktree/src/index/mod.rs +63 −0
git-worktree/src/index.rs+0−245
@@ -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−}
git-worktree/src/index/checkout.rs+73−0
@@ -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+}
git-worktree/src/index/entry.rs+109−0
@@ -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+}
git-worktree/src/index/mod.rs+63−0
@@ -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+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t