libpijul 0.12.0

A patch-based distributed version control system, easy to use and fast.
//! Layout of a repository (files in `.pijul`) on the disk. This
//! module exports both high-level functions that require no knowledge
//! of the repository, and lower-level constants documented on
//! [pijul.org/documentation/repository](https://pijul.org/documentation/repository),
//! used for instance for downloading files from remote repositories.

use super::Repository;
use backend::DEFAULT_BRANCH;
use backend::{Hash, HashRef};
use backend::{MutTxn, ROOT_INODE};
use bs58;
use flate2;
use ignore::overrides::OverrideBuilder;
use ignore::WalkBuilder;
use patch::{Patch, PatchHeader};
use rand::distributions::Alphanumeric;
use rand::Rng;
use std;
use std::fs::canonicalize;
use std::fs::{create_dir_all, metadata, File};
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use {Result, Error};
use std::ffi::OsStr;

/// Given a Path-like type P, RepoPath<P> is a 'P' relative to some fs_representation::RepoRoot
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub struct RepoPath<P: ?Sized>(pub P);

pub fn in_repo_root() -> RepoPath<&'static Path> {
    RepoPath(Path::new(""))
}

impl RepoPath<std::path::PathBuf> {
    pub fn push(&mut self, x: &str) {
        self.0.push(x)
    }

    pub fn pop(&mut self) -> bool {
        self.0.pop()
    }

    pub fn new() -> Self {
        RepoPath(PathBuf::new())
    }
    
    pub fn as_ref(&self) -> RepoPath<&Path> {
        RepoPath(self.0.as_ref())
    }

    pub fn set_file_name(&mut self, filename: &OsStr) {
        self.0.set_file_name(filename)
    }

    pub fn from_string(path: String) -> Self {
        RepoPath(PathBuf::from(path))
    }
}

    
impl<P: AsRef<Path>> RepoPath<P> {
    pub fn as_path(&self) -> &std::path::Path {
        self.0.as_ref()
    }
}

impl<P: AsRef<Path>> RepoPath<P> {    
    pub fn parent(&self) -> Option<RepoPath<&std::path::Path>> {
        self.as_path().parent().map(RepoPath)
    }

    pub fn file_name(&self) -> Option<&OsStr> {
        self.as_path().file_name()
    }

    pub fn split(&self) -> Option<(RepoPath<&std::path::Path>, &OsStr)> {
        self.parent().map(|p| {(p, self.file_name().expect("file_name and parent should be consistent"))})
    }

    pub fn components(&self) -> std::path::Components {
        self.as_path().components()
    }

    pub fn to_path_buf(&self) -> PathBuf {
        self.as_path().to_path_buf()
    }

    pub fn display(&self) -> std::path::Display {
        self.as_path().display()
    }

    
    pub fn to_owned(&self) -> RepoPath<PathBuf> {
        RepoPath(self.0.as_ref().to_path_buf())
    }    
    
    pub fn join(&self, path: &Path) -> RepoPath<PathBuf> {
        // TODO: check that the joined path is indeed inside the repo
        let joined_path = self.as_path().join(path);
        RepoPath(joined_path)
    }
}

impl<P: AsRef<Path>> RepoPath<P> {
    pub fn empty(&self) -> bool {
        self.as_path() == Path::new("")
    }
}

/// A directory at the root of a pijul repository.
#[derive(Clone, Copy, Debug)]
pub struct RepoRoot<P: AsRef<Path>> {
    pub repo_root: P,
}

/// Name of the root directory, i.e. `.pijul`.
pub const PIJUL_DIR_NAME: &'static str = ".pijul";

/// Basename of the changes file for branch `br`. This file is only
/// used when pulling/pushing over HTTP (where calling remote programs
/// to list patches is impossible).
///
/// The changes file contains the same information as the one returned by `pijul log --hash-only`.
pub fn branch_changes_base_path(b: &str) -> String {
    "changes.".to_string() + &bs58::encode(b.as_bytes()).into_string()
}

/// Basename of the patch corresponding to the given patch hash.
pub fn patch_file_name(hash: HashRef) -> String {
    hash.to_base58() + ".gz"
}

impl<P: AsRef<Path>> RepoRoot<P> {
    /// The subdirectory of `self` with pijul's metadata
    pub fn repo_dir(&self) -> PathBuf {
        self.repo_root.as_ref().join(PIJUL_DIR_NAME)
    }

    /// Directory where the pristine of `self` is.
    /// For instance, if the repository in in `/a/b`,
    /// `self.pristine_dir() is `/a/b/.pijul/pristine`.
    pub fn pristine_dir(&self) -> PathBuf {
        self.repo_dir().join("pristine")
    }

    /// Directory where the patches are. `patches_dir("/a/b") = "/a/b/.pijul/patches"`.
    pub fn patches_dir(&self) -> PathBuf {
        self.repo_dir().join("patches")
    }

    /// The location of the changes file for theb branch `b`.
    ///
    /// The changes file contains the same information as the one returned by `pijul log --hash-only`.
    pub fn branch_changes_file(&self, b: &str) -> PathBuf {
        self.repo_dir().join(branch_changes_base_path(b))
    }

    /// The meta file, where user preferences are stored.
    pub fn meta_file(&self) -> PathBuf {
        self.repo_dir().join("meta.toml")
    }

    /// The id file is used for remote operations, to identify a
    /// repository and save bandwidth when the remote state is partially
    /// known.
    pub fn id_file(&self) -> PathBuf {
        self.repo_dir().join("id")
    }

    /// Read a complete patch.
    pub fn read_patch(&self, hash: HashRef) -> Result<Patch> {
        let patch_dir = self.patches_dir();
        let path = patch_dir.join(&patch_file_name(hash));
        let f = File::open(path)?;
        let mut f = BufReader::new(f);
        let (_, _, patch) = Patch::from_reader_compressed(&mut f)?;
        Ok(patch)
    }

    /// Read a patch, but without the "changes" part, i.e. the actual
    /// contents of the patch.
    pub fn read_patch_nochanges(&self, hash: HashRef) -> Result<PatchHeader> {
        let patch_dir = self.patches_dir();
        let path = patch_dir.join(&patch_file_name(hash));
        let f = File::open(path)?;
        let mut f = flate2::bufread::GzDecoder::new(BufReader::new(f));
        Ok(PatchHeader::from_reader_nochanges(&mut f)?)
    }

    /// Read a patch, but without the "changes" part, i.e. the actual
    /// contents of the patch.
    pub fn read_dependencies(&self, hash: HashRef) -> Result<Vec<Hash>> {
        let patch_dir = self.patches_dir();
        let path = patch_dir.join(&patch_file_name(hash));
        let f = File::open(path)?;
        let mut f = flate2::bufread::GzDecoder::new(BufReader::new(f));
        Ok(Patch::read_dependencies(&mut f)?)
    }

    /// The ignore file that is _not_ tracked by pijul.
    pub fn local_ignore_file(&self) -> PathBuf {
        self.repo_dir().join("local").join("ignore")
    }

    pub fn get_current_branch(&self) -> Result<String> {
        let mut path = self.repo_dir();
        path.push("current_branch");
        if let Ok(mut f) = File::open(&path) {
            let mut s = String::new();
            f.read_to_string(&mut s)?;
            Ok(s.trim().to_string())
        } else {
            Ok(DEFAULT_BRANCH.to_string())
        }
    }

    pub fn set_current_branch(&self, branch: &str) -> Result<()> {
        let mut path = self.repo_dir();
        path.push("current_branch");
        let mut f = File::create(&path)?;
        f.write_all(branch.trim().as_ref())?;
        f.write_all(b"\n")?;
        Ok(())
    }

    pub fn open_repo(&self, increase: Option<u64>) -> Result<Repository> {
        Repository::open(self.pristine_dir(), increase)
    }

    
    pub fn relativize<'a>(&self, path: &'a Path) -> Result<RepoPath<&'a Path>> {
    	match path.strip_prefix(&self.repo_root) {
            Ok(p) => Ok(RepoPath(p)),
            Err(_) => Err(Error::FileNotInRepo(path.to_path_buf()))
        }
    }

    pub fn absolutize<'a>(&self, path: &RepoPath<impl AsRef<Path>>) -> PathBuf {
        self.repo_root.as_ref().join(path.as_path())
    }
}

impl<P: AsRef<Path> + 'static> RepoRoot<P> {
    pub fn untracked_files<T: rand::Rng, Q: AsRef<Path>>(
        &self,
        txn: &MutTxn<T>,
        path: Q,
    ) -> impl Iterator<Item = RepoPath<PathBuf>> + '_ {
        let known_files = txn.list_files(ROOT_INODE).unwrap_or_else(|_| vec![]);

        let o = OverrideBuilder::new(self.repo_root.as_ref())
            .add("!.pijul")
            .unwrap()
            .build()
            .unwrap(); // we can be pretty confident these two calls will
                       // not fail as the glob is hard-coded

        let mut w = WalkBuilder::new(path.as_ref());
        w.git_ignore(false)
            .git_exclude(false)
            .git_global(false)
            .hidden(false)
            .add_custom_ignore_filename(".pijulignore");

        // add .pijul/local/ignore
        w.add_ignore(self.local_ignore_file());
        w.overrides(o);

        w.build().filter_map(move |f| {
            if let Ok(f) = f {
                let p = f.path();
                if p == self.repo_root.as_ref() {
                    return None;
                }

                let p_in_repo = self.relativize(&p).unwrap();
                // ^- cannot fail since p must be within repo_root.
                if known_files.iter().any(|t| t.as_ref() == p_in_repo) {
                    return None
                }
                Some(p_in_repo.to_owned())
            } else {
                None
            }
        })
    }
}

/// Find the repository root from one of its descendant
/// directories. Return `None` iff `dir` is not in a repository.
pub fn find_repo_root<'a>(dir: &'a Path) -> Option<RepoRoot<PathBuf>> {
    let mut p = dir.to_path_buf();
    loop {
        p.push(PIJUL_DIR_NAME);
        match metadata(&p) {
            Ok(ref attr) if attr.is_dir() => {
                p.pop();
                return Some(RepoRoot { repo_root: p });
            }
            _ => {}
        }
        p.pop();

        if !p.pop() {
            return None;
        }
    }
}

#[doc(hidden)]
pub const ID_LENGTH: usize = 100;

/// Create a repository. `dir` must be the repository root (a
/// `".pijul"` directory will be created in `dir`).
pub fn create<R: Rng>(dir: &Path, mut rng: R) -> std::io::Result<RepoRoot<PathBuf>> {
    let r = RepoRoot {
        repo_root: canonicalize(dir)?,
    };

    let mut repo_dir = r.repo_dir();
    create_dir_all(&repo_dir)?;

    repo_dir.push("pristine");
    create_dir_all(&repo_dir)?;
    repo_dir.pop();

    repo_dir.push("patches");
    create_dir_all(&repo_dir)?;
    repo_dir.pop();

    repo_dir.push("id");
    let mut f = std::fs::File::create(&repo_dir)?;
    let mut x = String::new();
    x.extend(rng.sample_iter(&Alphanumeric).take(ID_LENGTH));
    f.write_all(x.as_bytes())?;
    repo_dir.pop();

    repo_dir.push("version");
    let mut f = std::fs::File::create(&repo_dir)?;
    writeln!(f, "{}", env!("CARGO_PKG_VERSION"))?;
    repo_dir.pop();

    repo_dir.push("local");
    create_dir_all(&repo_dir)?;
    repo_dir.pop();

    repo_dir.push("hooks");
    create_dir_all(&repo_dir)?;
    repo_dir.pop();

    repo_dir.push("local");
    repo_dir.push("ignore");
    std::fs::File::create(&repo_dir)?;
    repo_dir.pop();
    repo_dir.pop();

    Ok(r)
}