Skip to main content

pijul_core/working_copy/
filesystem.rs

1use super::*;
2use crate::pristine::{ArcTxn, GraphTxnT, InodeMetadata, TreeErr, TreeTxnT, TxnErr};
3use canonical_path::{CanonicalPath, CanonicalPathBuf};
4use ignore::WalkBuilder;
5use std::borrow::Cow;
6use std::path::{Path, PathBuf};
7
8#[derive(Clone)]
9pub struct FileSystem {
10    root: PathBuf,
11}
12
13/// Returns whether `path` is a child of `root_` (or `root_` itself).
14pub fn filter_ignore(root_: &CanonicalPath, path: &CanonicalPath, is_dir: bool) -> bool {
15    debug!("path = {:?} root = {:?}", path, root_);
16    if let Ok(suffix) = path.as_path().strip_prefix(root_.as_path()) {
17        debug!("suffix = {:?}", suffix);
18        let mut root = root_.as_path().to_path_buf();
19        let mut ignore = ignore::gitignore::GitignoreBuilder::new(&root);
20        let mut add_root = |root: &mut PathBuf| {
21            ignore.add_line(None, crate::DOT_DIR).unwrap();
22            root.push(".ignore");
23            ignore.add(&root);
24            root.pop();
25            root.push(".gitignore");
26            ignore.add(&root);
27            root.pop();
28        };
29        add_root(&mut root);
30        for c in suffix.components() {
31            root.push(c);
32            add_root(&mut root);
33        }
34        if let Ok(ig) = ignore.build() {
35            let m = ig.matched(suffix, is_dir);
36            debug!("m = {:?}", m);
37            return !m.is_ignore();
38        }
39    }
40    false
41}
42
43/// From a path on the filesystem, return the canonical path (a `PathBuf`), and a
44/// prefix relative to the root of the repository (a `String`).
45pub fn get_prefix(
46    repo_path: Option<&Path>,
47    prefix: &Path,
48) -> Result<(PathBuf, String), std::io::Error> {
49    let mut p = String::new();
50    let repo = if let Some(repo) = repo_path {
51        Cow::Borrowed(repo)
52    } else {
53        Cow::Owned(std::env::current_dir()?)
54    };
55    debug!("get prefix {:?} {:?}", repo, prefix);
56    let repo_prefix = &repo.join(prefix);
57    let prefix_ = if let Ok(x) = repo_prefix.canonicalize() {
58        x
59    } else {
60        let mut p = PathBuf::new();
61        for c in repo_prefix.components() {
62            use std::path::Component;
63            match c {
64                Component::Prefix(_) => p.push(c.as_os_str()),
65                Component::RootDir => p.push(c.as_os_str()),
66                Component::CurDir => {}
67                Component::ParentDir => {
68                    p.pop();
69                }
70                Component::Normal(x) => p.push(x),
71            }
72        }
73        p
74    };
75    debug!("get prefix {:?}", prefix_);
76    if let Ok(prefix) = prefix_.as_path().strip_prefix(repo) {
77        for c in prefix.components() {
78            if !p.is_empty() {
79                p.push('/');
80            }
81            let c: &std::path::Path = c.as_ref();
82            p.push_str(&c.to_string_lossy())
83        }
84    }
85    Ok((prefix_, p))
86}
87
88#[derive(Error)]
89pub enum AddError<T: GraphTxnT + TreeTxnT> {
90    #[error(transparent)]
91    Ignore(#[from] ignore::Error),
92    #[error(transparent)]
93    Io(#[from] std::io::Error),
94    #[error(transparent)]
95    Fs(#[from] crate::fs::FsError<T>),
96}
97
98impl<T: GraphTxnT + TreeTxnT> std::fmt::Debug for AddError<T> {
99    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
100        match self {
101            AddError::Ignore(e) => std::fmt::Debug::fmt(e, fmt),
102            AddError::Io(e) => std::fmt::Debug::fmt(e, fmt),
103            AddError::Fs(e) => std::fmt::Debug::fmt(e, fmt),
104        }
105    }
106}
107
108#[derive(Error)]
109pub enum Error<C: std::error::Error + 'static, T: GraphTxnT + TreeTxnT> {
110    #[error(transparent)]
111    Add(#[from] AddError<T>),
112    #[error(transparent)]
113    Record(#[from] crate::record::RecordError<C, std::io::Error, T>),
114    #[error(transparent)]
115    Txn(#[from] TxnErr<T::GraphError>),
116    #[error(transparent)]
117    Tree(#[from] TreeErr<T::TreeError>),
118}
119
120impl<C: std::error::Error + 'static, T: GraphTxnT + TreeTxnT> std::fmt::Debug for Error<C, T> {
121    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
122        match self {
123            Error::Add(e) => std::fmt::Debug::fmt(e, fmt),
124            Error::Record(e) => std::fmt::Debug::fmt(e, fmt),
125            Error::Txn(e) => std::fmt::Debug::fmt(e, fmt),
126            Error::Tree(e) => std::fmt::Debug::fmt(e, fmt),
127        }
128    }
129}
130
131pub struct Untracked {
132    join: Option<std::thread::JoinHandle<Result<(), std::io::Error>>>,
133    receiver: std::sync::mpsc::Receiver<(PathBuf, bool)>,
134}
135
136impl Iterator for Untracked {
137    type Item = Result<(PathBuf, bool), std::io::Error>;
138    fn next(&mut self) -> Option<Self::Item> {
139        if let Ok(x) = self.receiver.recv() {
140            return Some(Ok(x));
141        } else {
142            if let Some(j) = self.join.take()
143                && let Ok(Err(e)) = j.join()
144            {
145                return Some(Err(e));
146            }
147        }
148        None
149    }
150}
151
152impl FileSystem {
153    pub fn from_root<P: AsRef<Path>>(root: P) -> Self {
154        FileSystem {
155            root: root.as_ref().to_path_buf(),
156        }
157    }
158
159    #[allow(clippy::too_many_arguments)]
160    pub fn record_prefixes<
161        T: crate::MutTxnTExt + crate::TxnTExt + Send + Sync + 'static,
162        C: crate::changestore::ChangeStore + Clone + Send + 'static,
163        P: AsRef<Path>,
164    >(
165        &self,
166        txn: ArcTxn<T>,
167        algo: crate::Algorithm,
168        channel: crate::pristine::ChannelRef<T>,
169        changes: &C,
170        state: &mut crate::RecordBuilder,
171        repo_path: CanonicalPathBuf,
172        prefixes: &[P],
173        force: bool,
174        threads: usize,
175        salt: u64,
176    ) -> Result<(), Error<C::Error, T>>
177    where
178        T::Channel: Send + Sync,
179    {
180        for prefix in prefixes.iter() {
181            self.clone().record_prefix(
182                txn.clone(),
183                algo,
184                channel.clone(),
185                changes,
186                state,
187                repo_path.clone(),
188                prefix.as_ref(),
189                force,
190                threads,
191                salt,
192            )?
193        }
194        if prefixes.is_empty() {
195            self.record_prefix(
196                txn,
197                algo,
198                channel,
199                changes,
200                state,
201                repo_path.clone(),
202                Path::new(""),
203                force,
204                threads,
205                salt,
206            )?
207        }
208        Ok(())
209    }
210
211    pub fn add_prefix_rec<T: crate::MutTxnTExt + crate::TxnTExt>(
212        &self,
213        txn: &ArcTxn<T>,
214        repo_path: CanonicalPathBuf,
215        full: CanonicalPathBuf,
216        force: bool,
217        threads: usize,
218        salt: u64,
219    ) -> Result<(), AddError<T>> {
220        let mut txn = txn.write();
221        for p in
222            self.iterate_prefix_rec(repo_path.clone(), full.clone(), force, threads, |_, _| true)?
223        {
224            let (path, is_dir) = p?;
225            info!("Adding {:?}", path);
226            use path_slash::PathExt;
227            let path_str = path.to_slash_lossy();
228            if path_str.is_empty() || path_str == "." {
229                continue;
230            }
231            match txn.add(&path_str, is_dir, salt) {
232                Ok(_) => {}
233                Err(crate::fs::FsError::AlreadyInRepo(_)) => {}
234                Err(e) => return Err(e.into()),
235            }
236        }
237        Ok(())
238    }
239
240    pub fn iterate_prefix_rec<F: Fn(&Path, bool) -> bool + Send + Sync + 'static>(
241        &self,
242        repo_path: CanonicalPathBuf,
243        full: CanonicalPathBuf,
244        force: bool,
245        threads: usize,
246        follow: F,
247    ) -> Result<Untracked, std::io::Error> {
248        debug!("full = {:?}", full);
249        let meta = std::fs::metadata(&full)?;
250        debug!("meta = {:?}", meta);
251        let (sender, receiver) = std::sync::mpsc::sync_channel(100);
252
253        debug!("{:?}", full.as_path().strip_prefix(repo_path.as_path()));
254        debug!("force = {:?}", force);
255        if !force
256            && !filter_ignore(
257                repo_path.as_canonical_path(),
258                full.as_canonical_path(),
259                meta.is_dir(),
260            )
261        {
262            return Ok(Untracked {
263                join: None,
264                receiver,
265            });
266        }
267        let follow = std::sync::Arc::new(follow);
268        let t = std::thread::spawn(move || -> Result<(), std::io::Error> {
269            let follow = follow.clone();
270            if meta.is_dir() {
271                let mut walk = WalkBuilder::new(&full);
272                walk.ignore(!force)
273                    .git_ignore(!force)
274                    .hidden(false)
275                    .filter_entry(|p| {
276                        debug!("p.file_name = {:?}", p.file_name());
277                        p.file_name() != crate::DOT_DIR
278                    })
279                    .threads((threads - 1).max(1));
280                walk.build_parallel().run(move || {
281                    let repo_path = repo_path.clone();
282                    let sender = sender.clone();
283                    let follow = follow.clone();
284                    Box::new(move |entry| {
285                        let entry: ignore::DirEntry = if let Ok(entry) = entry {
286                            entry
287                        } else {
288                            return ignore::WalkState::Quit;
289                        };
290                        let p = entry.path();
291                        if let Some(p) = p.file_name()
292                            && let Some(p) = p.to_str()
293                            && (p.ends_with("~") || (p.starts_with("#") && p.ends_with("#")))
294                        {
295                            return ignore::WalkState::Skip;
296                        }
297                        debug!("entry path = {:?} {:?}", entry.path(), repo_path);
298                        if let Ok(path) = entry.path().strip_prefix(&repo_path) {
299                            let is_dir = entry.file_type().unwrap().is_dir();
300                            if sender.send((path.to_path_buf(), is_dir)).is_err() {
301                                return ignore::WalkState::Quit;
302                            }
303                            if !follow(path, is_dir) {
304                                return ignore::WalkState::Skip;
305                            }
306                        } else {
307                            debug!("entry = {:?}", entry.path());
308                        }
309                        ignore::WalkState::Continue
310                    })
311                })
312            } else {
313                debug!("filter_ignore ok");
314                let path = full.as_path().strip_prefix(repo_path.as_path()).unwrap();
315                sender.send((path.to_path_buf(), false)).unwrap();
316            }
317            Ok(())
318        });
319        Ok(Untracked {
320            join: Some(t),
321            receiver,
322        })
323    }
324
325    #[allow(clippy::too_many_arguments)]
326    pub fn record_prefix<
327        T: crate::MutTxnTExt + crate::TxnTExt + Send + Sync + 'static,
328        C: crate::changestore::ChangeStore + Clone + Send + 'static,
329    >(
330        &self,
331        txn: ArcTxn<T>,
332        algorithm: crate::Algorithm,
333        channel: crate::pristine::ChannelRef<T>,
334        changes: &C,
335        state: &mut crate::RecordBuilder,
336        repo_path: CanonicalPathBuf,
337        prefix: &Path,
338        force: bool,
339        threads: usize,
340        salt: u64,
341    ) -> Result<(), Error<C::Error, T>>
342    where
343        T::Channel: Send + Sync,
344    {
345        let (full, prefix) = get_prefix(Some(repo_path.as_ref()), prefix).map_err(AddError::Io)?;
346        if let Ok(full) = CanonicalPathBuf::canonicalize(&full)
347            && let Ok(path) = full.as_path().strip_prefix(repo_path.as_path())
348        {
349            use path_slash::PathExt;
350            let path_str = path.to_slash_lossy();
351            if !crate::fs::is_tracked(&*txn.read(), &path_str)? {
352                self.add_prefix_rec(&txn, repo_path, full, force, threads, salt)?;
353            }
354        }
355        debug!("recording from prefix {:?}", prefix);
356        state.record(
357            txn.clone(),
358            algorithm,
359            false,
360            &crate::diff::DEFAULT_SEPARATOR,
361            channel,
362            self,
363            changes,
364            &prefix,
365            threads,
366        )?;
367        debug!("recorded");
368        Ok(())
369    }
370
371    fn path(&self, file: &str) -> PathBuf {
372        let mut path = self.root.clone();
373        path.extend(crate::path::components(file));
374        path
375    }
376}
377
378impl WorkingCopyRead for FileSystem {
379    type Error = std::io::Error;
380    fn file_metadata(&self, file: &str) -> Result<InodeMetadata, Self::Error> {
381        debug!("metadata {:?}", file);
382        let attr = std::fs::metadata(self.path(file))?;
383        let permissions = permissions(&attr).unwrap_or(0o700);
384        debug!("permissions = {:?}", permissions);
385        Ok(InodeMetadata::new(permissions & 0o100, attr.is_dir()))
386    }
387    fn read_file(&self, file: &str, buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
388        use std::io::Read;
389        debug!("read_file {:?}", file);
390        let mut f = std::fs::File::open(self.path(file))?;
391        f.read_to_end(buffer)?;
392        Ok(())
393    }
394
395    #[cfg(not(unix))]
396    fn modified_time(&self, file: &str) -> Result<std::time::SystemTime, Self::Error> {
397        debug!("modified_time {:?}", file);
398        let attr = std::fs::metadata(&self.path(file))?;
399        Ok(attr.modified()?)
400    }
401
402    #[cfg(unix)]
403    fn modified_time(&self, file: &str) -> Result<std::time::SystemTime, Self::Error> {
404        debug!("modified_time {:?}", file);
405        use std::os::unix::fs::MetadataExt;
406        let attr = std::fs::metadata(self.path(file))?;
407        let ctime = std::time::SystemTime::UNIX_EPOCH
408            + std::time::Duration::from_millis(
409                attr.ctime() as u64 * 1000 + attr.ctime_nsec() as u64 / 1_000_000,
410            );
411        Ok(attr.modified()?.max(ctime))
412    }
413}
414
415impl WorkingCopy for FileSystem {
416    fn create_dir_all(&self, file: &str) -> Result<(), Self::Error> {
417        debug!("create_dir_all {:?}", file);
418        std::fs::create_dir_all(self.path(file))
419    }
420
421    fn remove_path(&self, path: &str, rec: bool) -> Result<(), Self::Error> {
422        debug!("remove_path {:?}", path);
423        let path = self.path(path);
424        if let Ok(meta) = std::fs::metadata(&path)
425            && let Err(e) = if meta.is_dir() {
426                if rec {
427                    std::fs::remove_dir_all(&path)
428                } else {
429                    std::fs::remove_dir(&path)
430                }
431            } else {
432                std::fs::remove_file(&path)
433            }
434        {
435            info!("while deleting {:?}: {:?}", path, e);
436        }
437        Ok(())
438    }
439    fn rename(&self, former: &str, new: &str) -> Result<(), Self::Error> {
440        debug!("rename {:?} {:?}", former, new);
441        let former = self.path(former);
442        let new = self.path(new);
443        if let Some(p) = new.parent() {
444            std::fs::create_dir_all(p)?
445        }
446        std::fs::rename(&former, &new)?;
447        Ok(())
448    }
449    #[cfg(not(windows))]
450    fn set_permissions(&self, name: &str, permissions: u16) -> Result<(), Self::Error> {
451        use std::os::unix::fs::PermissionsExt;
452        let name = self.path(name);
453        debug!("set_permissions: {:?}", name);
454        let metadata = std::fs::metadata(&name)?;
455        let mut current = metadata.permissions();
456        debug!(
457            "setting mode for {:?} to {:?} (currently {:?})",
458            name, permissions, current
459        );
460        if permissions & 0o100 != 0 {
461            current.set_mode(current.mode() | 0o100);
462        } else {
463            current.set_mode(current.mode() & ((!0o777) | 0o666));
464        }
465        debug!("setting {:?}", current);
466        std::fs::set_permissions(name, current)?;
467        debug!("set");
468        Ok(())
469    }
470    #[cfg(windows)]
471    fn set_permissions(&self, _name: &str, _permissions: u16) -> Result<(), Self::Error> {
472        Ok(())
473    }
474
475    type Writer = std::io::BufWriter<std::fs::File>;
476    fn write_file(&self, file: &str, _: Inode) -> Result<Self::Writer, Self::Error> {
477        let path = self.path(file);
478        debug!("path = {:?}", path);
479        if let Some(p) = path.parent() {
480            std::fs::create_dir_all(p).unwrap_or(())
481        }
482        debug!("write_file: dir created");
483        std::fs::remove_file(&path).unwrap_or(());
484        let file = std::io::BufWriter::new(std::fs::File::create(&path)?);
485        debug!("file");
486        Ok(file)
487    }
488
489    fn touch(&self, file: &str, time: std::time::SystemTime) -> Result<(), Self::Error> {
490        let p = self.path(file);
491        // let src = std::fs::metadata(&p)?;
492        let dest = std::fs::File::open(&p)?;
493        let times = std::fs::FileTimes::new().set_modified(time);
494        dest.set_times(times)?;
495        Ok(())
496    }
497}
498
499#[cfg(not(windows))]
500fn permissions(attr: &std::fs::Metadata) -> Option<usize> {
501    use std::os::unix::fs::PermissionsExt;
502    Some(attr.permissions().mode() as usize)
503}
504#[cfg(windows)]
505fn permissions(_: &std::fs::Metadata) -> Option<usize> {
506    None
507}