9.4 KiB, 29 runs
created by r2848102244:19, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Where a rendered tree lands on a filesystem. |
| 2 | //! |
| 3 | //! What a tree *is* -- the render, the layout, and the policy that decides which |
| 4 | //! of two files claiming one path keeps the name -- is in [`ore_store::tree`], |
| 5 | //! because the forge shows the same names on a page as this puts on a disk and |
| 6 | //! one policy implemented twice is a policy that drifts. It is re-exported here |
| 7 | //! so that `tree::Tree` still means what it always meant in this crate. |
| 8 | //! |
| 9 | //! What is left in this module is the half only a tool with a working copy has |
| 10 | //! any use for: turning a path the history carries into a place on the |
| 11 | //! filesystem, writing a tree there, and saying what moved. |
| 12 | |
| 13 | use crate::capture; |
| 14 | use crate::repo::IGNORE_FILE; |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | use oxedyne_fe2o3_ore::op::Mode; |
| 18 | |
| 19 | // The tree, the layout and the clash policy, which live in the shared store |
| 20 | // crate and are named here as they always were. |
| 21 | pub use ore_store::tree::{ |
| 22 | ancestry, |
| 23 | at, |
| 24 | derived_name, |
| 25 | file_reached, |
| 26 | shown, |
| 27 | whole, |
| 28 | Clash, |
| 29 | Layout, |
| 30 | Tree, |
| 31 | CLASH_MARK, |
| 32 | }; |
| 33 | |
| 34 | use std::ffi::OsStr; |
| 35 | use std::fs; |
| 36 | use std::path::{ |
| 37 | Path, |
| 38 | PathBuf, |
| 39 | }; |
| 40 | |
| 41 | |
| 42 | /// Turns a path the history carries into a place on the filesystem. |
| 43 | /// |
| 44 | /// The bytes are written as they stand rather than through a string, so a path |
| 45 | /// git permits and UTF-8 does not still lands where it says. What is refused is a |
| 46 | /// path that would leave the working copy: an empty one, an absolute one, or one |
| 47 | /// climbing through a parent directory. |
| 48 | pub fn on_disk(root: &Path, path: &[u8]) |
| 49 | -> Outcome<PathBuf> |
| 50 | { |
| 51 | if path.is_empty() { |
| 52 | return Err(err!( |
| 53 | "A file carries an empty path, which names nowhere in a working copy."; |
| 54 | Invalid, Data, Missing)); |
| 55 | } |
| 56 | if path[0] == b'/' { |
| 57 | return Err(err!( |
| 58 | "The path {:?} is absolute, and a working copy holds only what is beneath \ |
| 59 | it.", shown(path); |
| 60 | Invalid, Data, Range)); |
| 61 | } |
| 62 | for part in path.split(|b| *b == b'/') { |
| 63 | if part == b".." { |
| 64 | return Err(err!( |
| 65 | "The path {:?} climbs out of the working copy.", shown(path); |
| 66 | Invalid, Data, Range)); |
| 67 | } |
| 68 | } |
| 69 | Ok(root.join(res!(os_path(path)))) |
| 70 | } |
| 71 | |
| 72 | /// Turns path bytes into a filesystem path. |
| 73 | #[cfg(unix)] |
| 74 | fn os_path(path: &[u8]) |
| 75 | -> Outcome<PathBuf> |
| 76 | { |
| 77 | use std::os::unix::ffi::OsStrExt; |
| 78 | Ok(PathBuf::from(OsStr::from_bytes(path))) |
| 79 | } |
| 80 | |
| 81 | /// Turns path bytes into a filesystem path, where only UTF-8 can be spelled. |
| 82 | #[cfg(not(unix))] |
| 83 | fn os_path(path: &[u8]) |
| 84 | -> Outcome<PathBuf> |
| 85 | { |
| 86 | match std::str::from_utf8(path) { |
| 87 | Ok(s) => Ok(PathBuf::from(s)), |
| 88 | Err(e) => Err(err!(e, |
| 89 | "The path {:?} is not UTF-8, and this platform's filesystem names are.", |
| 90 | shown(path); |
| 91 | Invalid, Data, String)), |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | /// Returns the bytes of a filesystem name. |
| 96 | #[cfg(unix)] |
| 97 | pub fn name_bytes(name: &OsStr) |
| 98 | -> Outcome<Vec<u8>> |
| 99 | { |
| 100 | use std::os::unix::ffi::OsStrExt; |
| 101 | Ok(name.as_bytes().to_vec()) |
| 102 | } |
| 103 | |
| 104 | /// Returns the bytes of a filesystem name, where only UTF-8 can be spelled. |
| 105 | #[cfg(not(unix))] |
| 106 | pub fn name_bytes(name: &OsStr) |
| 107 | -> Outcome<Vec<u8>> |
| 108 | { |
| 109 | match name.to_str() { |
| 110 | Some(s) => Ok(s.as_bytes().to_vec()), |
| 111 | None => Err(err!( |
| 112 | "The name {:?} is not UTF-8, and this platform's filesystem names are.", |
| 113 | name; |
| 114 | Invalid, Data, String)), |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | |
| 119 | /// Puts a file on disk into the mode the history says it is. |
| 120 | /// |
| 121 | /// A symbolic link is refused rather than written as a regular file holding the |
| 122 | /// link target. The vocabulary records one and a mirror can write one back out, |
| 123 | /// but this working copy cannot yet hold one: the scan reads regular files only, |
| 124 | /// so a link written here would look like a deletion to the very next verb. That |
| 125 | /// is the wrong kind of nothing, and no repository on the replacement path holds |
| 126 | /// a symbolic link, so the honest answer is to say so and stop. |
| 127 | #[cfg(unix)] |
| 128 | fn set_mode(full: &Path, path: &[u8], mode: Mode) |
| 129 | -> Outcome<()> |
| 130 | { |
| 131 | use std::os::unix::fs::PermissionsExt; |
| 132 | if let Mode::Symlink = mode { |
| 133 | return Err(err!( |
| 134 | "The history says {:?} is a symbolic link, and a working copy here holds \ |
| 135 | regular files only. The link is safe in the log; nothing is written for it.", |
| 136 | shown(path); |
| 137 | Invalid, Data, Unimplemented)); |
| 138 | } |
| 139 | let meta = match fs::metadata(full) { |
| 140 | Ok(m) => m, |
| 141 | Err(e) => return Err(err!(e, |
| 142 | "The file {:?} could not be measured.", full; |
| 143 | IO, File, Read)), |
| 144 | }; |
| 145 | let was = meta.permissions().mode(); |
| 146 | // Executable wherever readable, which is what git's own checkout arrives at |
| 147 | // from a file the umask made: 0644 becomes 0755, and 0600 becomes 0700. |
| 148 | let want = match mode { |
| 149 | Mode::Executable => was | ((was & 0o444) >> 2), |
| 150 | _ => was & !0o111, |
| 151 | }; |
| 152 | if want == was { |
| 153 | return Ok(()); |
| 154 | } |
| 155 | match fs::set_permissions(full, fs::Permissions::from_mode(want)) { |
| 156 | Ok(()) => Ok(()), |
| 157 | Err(e) => Err(err!(e, |
| 158 | "The mode of {:?} could not be set to {:o}.", full, want; |
| 159 | IO, File, Write)), |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /// Puts a file into the mode the history says it is, where the filesystem |
| 164 | /// carries no such thing. |
| 165 | #[cfg(not(unix))] |
| 166 | fn set_mode(_full: &Path, path: &[u8], mode: Mode) |
| 167 | -> Outcome<()> |
| 168 | { |
| 169 | if let Mode::Symlink = mode { |
| 170 | return Err(err!( |
| 171 | "The history says {:?} is a symbolic link, which this platform's working \ |
| 172 | copy cannot hold.", shown(path); |
| 173 | Invalid, Data, Unimplemented)); |
| 174 | } |
| 175 | Ok(()) |
| 176 | } |
| 177 | |
| 178 | |
| 179 | /// What materialising a tree did to the working copy. |
| 180 | pub struct Moved { |
| 181 | /// Paths written that were not there before. |
| 182 | pub created: Vec<Vec<u8>>, |
| 183 | /// Paths whose bytes changed. |
| 184 | pub updated: Vec<Vec<u8>>, |
| 185 | /// Paths removed, because the state being materialised has no such file. |
| 186 | pub removed: Vec<Vec<u8>>, |
| 187 | /// How many paths were already exactly right. |
| 188 | pub unchanged: usize, |
| 189 | /// The paths two live files claimed, and the names the losers took. |
| 190 | pub clashes: Vec<Clash>, |
| 191 | } |
| 192 | |
| 193 | impl Moved { |
| 194 | /// Reports whether nothing at all moved. |
| 195 | pub fn is_empty(&self) -> bool { |
| 196 | self.created.is_empty() && self.updated.is_empty() && self.removed.is_empty() |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | /// What becomes of a file the working copy holds and the rendered tree does not. |
| 201 | pub enum Surplus { |
| 202 | Remove, // every verb that captures first, so the file is already in the history |
| 203 | Keep, // `import`, which captures nothing, so removing one would lose it |
| 204 | } |
| 205 | |
| 206 | /// Writes a rendered tree into the working copy, and says what moved. |
| 207 | /// |
| 208 | /// Under [`Surplus::Remove`] a file the tree does not describe is removed, since |
| 209 | /// the point of the exercise is to see that state and not a mixture of two, and |
| 210 | /// nothing is lost by it: the capture the verb began with has already recorded |
| 211 | /// whatever was there. **That reasoning holds only for a caller that captured |
| 212 | /// first.** `import` does not, so it renders under [`Surplus::Keep`] and takes |
| 213 | /// nothing off the disk that it never wrote down. |
| 214 | /// |
| 215 | /// The ignore file is never removed under either. It is not content so much as |
| 216 | /// the boundary of what the next capture will read, and a history only grows -- |
| 217 | /// so withdrawing it is not a past state being shown but the next verb being |
| 218 | /// handed the whole of `target/` to record for ever. Rendering a state that |
| 219 | /// predates the rules leaves them in place and says so. |
| 220 | /// |
| 221 | /// A file's mode is written whether or not its bytes changed. A script whose |
| 222 | /// executable bit has been cleared on disk is not the file the history says it |
| 223 | /// is, even where every byte of it matches, so the bit is asserted on every |
| 224 | /// path of the tree rather than only on the ones being rewritten. |
| 225 | pub fn materialise(root: &Path, tree: &Tree, surplus: Surplus) |
| 226 | -> Outcome<Moved> |
| 227 | { |
| 228 | let ignore = res!(capture::Ignore::read(root)); |
| 229 | let layout = res!(tree.layout()); |
| 230 | let want = res!(tree.contents()); |
| 231 | let modes = res!(tree.modes()); |
| 232 | // Tracked paths are read past the ignore rules, so a tracked file a rule |
| 233 | // happens to match is compared rather than blindly rewritten. |
| 234 | let (disk, _) = res!(capture::scan(root, &ignore, want.keys())); |
| 235 | let mut moved = Moved { |
| 236 | created: Vec::new(), |
| 237 | updated: Vec::new(), |
| 238 | removed: Vec::new(), |
| 239 | unchanged: 0, |
| 240 | clashes: layout.clashes, |
| 241 | }; |
| 242 | for (path, bytes) in &want { |
| 243 | let full = res!(on_disk(root, path)); |
| 244 | let mode = modes.get(path).copied().unwrap_or_default(); |
| 245 | match disk.get(path) { |
| 246 | Some(have) if have == bytes => { |
| 247 | moved.unchanged += 1; |
| 248 | res!(set_mode(&full, path, mode)); |
| 249 | continue; |
| 250 | }, |
| 251 | Some(_) => moved.updated.push(path.clone()), |
| 252 | None => moved.created.push(path.clone()), |
| 253 | } |
| 254 | if let Some(parent) = full.parent() { |
| 255 | match fs::create_dir_all(parent) { |
| 256 | Ok(()) => (), |
| 257 | Err(e) => return Err(err!(e, |
| 258 | "The directory {:?} could not be created.", parent; |
| 259 | IO, File, Write)), |
| 260 | } |
| 261 | } |
| 262 | match fs::write(&full, bytes) { |
| 263 | Ok(()) => (), |
| 264 | Err(e) => return Err(err!(e, |
| 265 | "The file {:?} could not be written.", full; |
| 266 | IO, File, Write)), |
| 267 | } |
| 268 | res!(set_mode(&full, path, mode)); |
| 269 | } |
| 270 | for path in disk.keys() { |
| 271 | if want.contains_key(path) { |
| 272 | continue; |
| 273 | } |
| 274 | if let Surplus::Keep = surplus { |
| 275 | continue; |
| 276 | } |
| 277 | // The rules that bound the next capture outlive any state being shown. |
| 278 | if path.as_slice() == IGNORE_FILE.as_bytes() { |
| 279 | continue; |
| 280 | } |
| 281 | let full = res!(on_disk(root, path)); |
| 282 | match fs::remove_file(&full) { |
| 283 | Ok(()) => (), |
| 284 | Err(e) => return Err(err!(e, |
| 285 | "The file {:?} could not be removed.", full; |
| 286 | IO, File, Write)), |
| 287 | } |
| 288 | moved.removed.push(path.clone()); |
| 289 | // A directory left empty by the removal is not part of the state either, |
| 290 | // and leaving it behind would show a tree the history does not describe. |
| 291 | let mut at = full.parent().map(|p| p.to_path_buf()); |
| 292 | while let Some(dir) = at { |
| 293 | if dir == root || fs::remove_dir(&dir).is_err() { |
| 294 | break; |
| 295 | } |
| 296 | at = dir.parent().map(|p| p.to_path_buf()); |
| 297 | } |
| 298 | } |
| 299 | moved.created.sort(); |
| 300 | moved.updated.sort(); |
| 301 | moved.removed.sort(); |
| 302 | Ok(moved) |
| 303 | } |