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 @@
1use git_hash::oid;
2
3use crate::{index, index::checkout::Collision};
4
5pub 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
81pub 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>
87where
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
134pub(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 @@
1use bstr::BString;
2use quick_error::quick_error;
3
4#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
5pub 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
12pub struct Outcome {
13 pub collisions: Vec<Collision>,
14}
15
16#[derive(Clone, Copy)]
17pub 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
41impl 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
53quick_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 @@
1use std::{
2 convert::TryInto,
3 fs::{create_dir_all, OpenOptions},
4 io::Write,
5 time::Duration,
6};
7
8use bstr::BStr;
9use git_hash::oid;
10use git_index::Entry;
11
12use crate::index;
13
14#[cfg_attr(not(unix), allow(unused_variables))]
15pub 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>
31where
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
89fn 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 @@
1use git_hash::oid;
2
3use crate::{index, index::checkout::Collision};
4
5pub mod checkout;
6pub(crate) mod entry;
7
8pub 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>
14where
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