Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/std-path'
Fintan Halpenny committed 3 years ago
commit 0ad95dbdfe9fdf81938ca419cf740469173e2022
parent 887cdc8
20 files changed +316 -919
modified radicle-surf/examples/diff.rs
@@ -58,13 +58,13 @@ fn init_repository_or_exit(path_to_repo: &str) -> git::Repository {

fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
    diff.created.iter().for_each(|created| {
-
        println!("+++ {}", created.path);
+
        println!("+++ {:?}", created.path);
    });
    diff.deleted.iter().for_each(|deleted| {
-
        println!("--- {}", deleted.path);
+
        println!("--- {:?}", deleted.path);
    });
    diff.modified.iter().for_each(|modified| {
-
        println!("mod {}", modified.path);
+
        println!("mod {:?}", modified.path);
    });

    println!(
modified radicle-surf/src/commit.rs
@@ -17,6 +17,8 @@

//! Represents a commit.

+
use std::path::PathBuf;
+

use git_ref_format::RefString;
#[cfg(feature = "serialize")]
use serde::{
@@ -26,7 +28,6 @@ use serde::{

use crate::{
    diff,
-
    file_system,
    git::{self, glob, Glob, Repository},
    person::Person,
    revision::Revision,
@@ -245,10 +246,6 @@ pub fn commits(repo: &Repository, maybe_revision: Option<Revision>) -> Result<Co
/// An error reported by commit API.
#[derive(Debug, thiserror::Error)]
pub enum Error {
-
    /// An error occurred during a file system operation.
-
    #[error(transparent)]
-
    FileSystem(#[from] file_system::Error),
-

    /// An error occurred during a git operation.
    #[error(transparent)]
    Git(#[from] git::Error),
@@ -258,5 +255,5 @@ pub enum Error {

    /// Trying to find a file path which could not be found.
    #[error("the path '{0}' was not found")]
-
    PathNotFound(file_system::Path),
+
    PathNotFound(PathBuf),
}
modified radicle-surf/src/diff.rs
@@ -17,13 +17,13 @@

#![allow(dead_code, unused_variables, missing_docs)]

-
use std::{convert::TryFrom, slice};
+
use std::{convert::TryFrom, path::PathBuf, slice};

#[cfg(feature = "serialize")]
use serde::{ser, Serialize, Serializer};

use crate::{
-
    file_system::{Directory, Path},
+
    file_system::Directory,
    git::{Error, Repository},
};

@@ -52,14 +52,14 @@ impl Default for Diff {
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreateFile {
-
    pub path: Path,
+
    pub path: PathBuf,
    pub diff: FileDiff,
}

#[cfg_attr(feature = "serialize", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeleteFile {
-
    pub path: Path,
+
    pub path: PathBuf,
    pub diff: FileDiff,
}

@@ -70,8 +70,8 @@ pub struct DeleteFile {
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MoveFile {
-
    pub old_path: Path,
-
    pub new_path: Path,
+
    pub old_path: PathBuf,
+
    pub new_path: PathBuf,
}

#[cfg_attr(
@@ -81,8 +81,8 @@ pub struct MoveFile {
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CopyFile {
-
    pub old_path: Path,
-
    pub new_path: Path,
+
    pub old_path: PathBuf,
+
    pub new_path: PathBuf,
}

#[cfg_attr(
@@ -104,7 +104,7 @@ pub enum EofNewLine {
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModifiedFile {
-
    pub path: Path,
+
    pub path: PathBuf,
    pub diff: FileDiff,
    pub eof: Option<EofNewLine>,
}
@@ -309,7 +309,7 @@ impl Diff {

    pub(crate) fn add_modified_file(
        &mut self,
-
        path: Path,
+
        path: PathBuf,
        hunks: impl Into<Hunks>,
        eof: Option<EofNewLine>,
    ) {
@@ -325,15 +325,15 @@ impl Diff {
        });
    }

-
    pub(crate) fn add_moved_file(&mut self, old_path: Path, new_path: Path) {
+
    pub(crate) fn add_moved_file(&mut self, old_path: PathBuf, new_path: PathBuf) {
        self.moved.push(MoveFile { old_path, new_path });
    }

-
    pub(crate) fn add_copied_file(&mut self, old_path: Path, new_path: Path) {
+
    pub(crate) fn add_copied_file(&mut self, old_path: PathBuf, new_path: PathBuf) {
        self.copied.push(CopyFile { old_path, new_path });
    }

-
    pub(crate) fn add_modified_binary_file(&mut self, path: Path) {
+
    pub(crate) fn add_modified_binary_file(&mut self, path: PathBuf) {
        self.modified.push(ModifiedFile {
            path,
            diff: FileDiff::Binary,
@@ -341,11 +341,11 @@ impl Diff {
        });
    }

-
    pub(crate) fn add_created_file(&mut self, path: Path, diff: FileDiff) {
+
    pub(crate) fn add_created_file(&mut self, path: PathBuf, diff: FileDiff) {
        self.created.push(CreateFile { path, diff });
    }

-
    pub(crate) fn add_deleted_file(&mut self, path: Path, diff: FileDiff) {
+
    pub(crate) fn add_deleted_file(&mut self, path: PathBuf, diff: FileDiff) {
        self.deleted.push(DeleteFile { path, diff });
    }

modified radicle-surf/src/diff/git.rs
@@ -17,15 +17,12 @@

use std::convert::TryFrom;

-
use crate::{
-
    diff::{self, Diff, EofNewLine, Hunk, Hunks, Line, LineDiff},
-
    file_system::Path,
-
};
+
use crate::diff::{self, Diff, EofNewLine, Hunk, Hunks, Line, LineDiff};

pub mod error {
-
    use thiserror::Error;
+
    use std::path::PathBuf;

-
    use crate::file_system::{self, Path};
+
    use thiserror::Error;

    #[derive(Debug, Error, PartialEq, Eq)]
    #[non_exhaustive]
@@ -54,8 +51,6 @@ pub mod error {
        #[error("git delta type is not handled")]
        DeltaUnhandled(git2::Delta),
        #[error(transparent)]
-
        FileSystem(#[from] file_system::Error),
-
        #[error(transparent)]
        Git(#[from] git2::Error),
        #[error(transparent)]
        Hunk(#[from] Hunk),
@@ -63,7 +58,7 @@ pub mod error {
        Line(#[from] LineDiff),
        /// A patch is unavailable.
        #[error("couldn't retrieve patch for {0}")]
-
        PatchUnavailable(Path),
+
        PatchUnavailable(PathBuf),
        /// A The path of a file isn't available.
        #[error("couldn't retrieve file path")]
        PathUnavailable,
@@ -95,8 +90,10 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
            match delta.status() {
                Delta::Added => {
                    let diff_file = delta.new_file();
-
                    let path = diff_file.path().ok_or(error::Diff::PathUnavailable)?;
-
                    let path = Path::try_from(path.to_path_buf())?;
+
                    let path = diff_file
+
                        .path()
+
                        .ok_or(error::Diff::PathUnavailable)?
+
                        .to_path_buf();

                    let patch = Patch::from_diff(&git_diff, idx)?;
                    if let Some(patch) = patch {
@@ -117,9 +114,10 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
                },
                Delta::Deleted => {
                    let diff_file = delta.old_file();
-
                    let path = diff_file.path().ok_or(error::Diff::PathUnavailable)?;
-
                    let path = Path::try_from(path.to_path_buf())?;
-

+
                    let path = diff_file
+
                        .path()
+
                        .ok_or(error::Diff::PathUnavailable)?
+
                        .to_path_buf();
                    let patch = Patch::from_diff(&git_diff, idx)?;
                    if let Some(patch) = patch {
                        diff.add_deleted_file(
@@ -139,9 +137,10 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
                },
                Delta::Modified => {
                    let diff_file = delta.new_file();
-
                    let path = diff_file.path().ok_or(error::Diff::PathUnavailable)?;
-
                    let path = Path::try_from(path.to_path_buf())?;
-

+
                    let path = diff_file
+
                        .path()
+
                        .ok_or(error::Diff::PathUnavailable)?
+
                        .to_path_buf();
                    let patch = Patch::from_diff(&git_diff, idx)?;

                    if let Some(patch) = patch {
@@ -200,10 +199,7 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
                        .path()
                        .ok_or(error::Diff::PathUnavailable)?;

-
                    let old_path = Path::try_from(old.to_path_buf())?;
-
                    let new_path = Path::try_from(new.to_path_buf())?;
-

-
                    diff.add_moved_file(old_path, new_path);
+
                    diff.add_moved_file(old.to_path_buf(), new.to_path_buf());
                },
                Delta::Copied => {
                    let old = delta
@@ -215,10 +211,7 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
                        .path()
                        .ok_or(error::Diff::PathUnavailable)?;

-
                    let old_path = Path::try_from(old.to_path_buf())?;
-
                    let new_path = Path::try_from(new.to_path_buf())?;
-

-
                    diff.add_copied_file(old_path, new_path);
+
                    diff.add_copied_file(old.to_path_buf(), new.to_path_buf());
                },
                status => {
                    return Err(error::Diff::DeltaUnhandled(status));
modified radicle-surf/src/file_system.rs
@@ -112,8 +112,3 @@

pub mod directory;
pub use directory::{Directory, Entries, Entry, File, FileContent};
-
mod error;
-
pub use error::Error;
-
mod path;
-

-
pub use self::path::*;
modified radicle-surf/src/file_system/directory.rs
@@ -22,18 +22,15 @@

use std::{
    collections::BTreeMap,
-
    convert::{Infallible, Into},
-
    path,
+
    convert::{Infallible, Into as _},
+
    path::{Path, PathBuf},
};

use git2::Blob;
use radicle_git_ext::{is_not_found_err, Oid};
use radicle_std_ext::result::ResultExt as _;

-
use crate::{
-
    file_system::{path::*, Error},
-
    git::{Repository, Revision},
-
};
+
use crate::git::{Repository, Revision};

pub mod error {
    use thiserror::Error;
@@ -52,8 +49,6 @@ pub mod error {
    pub enum Entry {
        #[error("the entry name was not valid UTF-8")]
        Utf8Error,
-
        #[error(transparent)]
-
        Label(#[from] super::Error),
    }

    #[derive(Debug, Error, PartialEq)]
@@ -74,11 +69,32 @@ pub mod error {
/// [`File::content`].
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct File {
-
    pub(crate) name: Label,
-
    pub(crate) id: Oid,
+
    /// The name of the file.
+
    name: String,
+
    /// The relative path of the file, not including the `name`,
+
    /// in respect to the root of the git repository.
+
    prefix: PathBuf,
+
    /// The object identifier of the git blob of this file.
+
    id: Oid,
}

impl File {
+
    /// Construct a new `File`.
+
    ///
+
    /// The `path` must be the prefix location of the directory, and
+
    /// so should not end in `name`.
+
    ///
+
    /// The `id` must point to a git blob.
+
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+
        debug_assert!(
+
            !prefix.ends_with(&name),
+
            "prefix = {:?}, name = {}",
+
            prefix,
+
            name
+
        );
+
        Self { name, prefix, id }
+
    }
+

    /// The name of this `File`.
    pub fn name(&self) -> &str {
        self.name.as_str()
@@ -89,17 +105,18 @@ impl File {
        self.id
    }

-
    /// Create a new `File` with the `name` and `id` provided.
+
    /// Return the exact path for this `File`, including the `name` of
+
    /// the directory itself.
    ///
-
    /// The `id` must point to a `git` blob.
-
    pub fn new(name: String, id: Oid) -> Self {
-
        Self {
-
            name: Label {
-
                label: name,
-
                hidden: false,
-
            },
-
            id,
-
        }
+
    /// The path is relative to the git repository root.
+
    pub fn path(&self) -> PathBuf {
+
        self.prefix.join(&self.name)
+
    }
+

+
    /// Return the [`Path`] where this `File` is located, relative to the
+
    /// git repository root.
+
    pub fn location(&self) -> &Path {
+
        &self.prefix
    }

    /// Get the [`FileContent`] for this `File`.
@@ -140,12 +157,12 @@ impl<'a> FileContent<'a> {

/// A representations of a [`Directory`]'s entries.
pub struct Entries {
-
    listing: BTreeMap<Label, Entry>,
+
    listing: BTreeMap<String, Entry>,
}

impl Entries {
-
    /// Return the [`Label`]s of each [`Entry`].
-
    pub fn names(&self) -> impl Iterator<Item = &Label> {
+
    /// Return the name of each [`Entry`].
+
    pub fn names(&self) -> impl Iterator<Item = &String> {
        self.listing.keys()
    }

@@ -154,8 +171,8 @@ impl Entries {
        self.listing.values()
    }

-
    /// Return each [`Label`] and [`Entry`].
-
    pub fn iter(&self) -> impl Iterator<Item = (&Label, &Entry)> {
+
    /// Return each [`Entry`] and its name.
+
    pub fn iter(&self) -> impl Iterator<Item = (&String, &Entry)> {
        self.listing.iter()
    }
}
@@ -172,22 +189,22 @@ pub enum Entry {
impl Entry {
    /// Get a label for the `Entriess`, either the name of the [`File`]
    /// or the name of the [`Directory`].
-
    pub fn label(&self) -> &Label {
+
    pub fn name(&self) -> &String {
        match self {
            Entry::File(file) => &file.name,
            Entry::Directory(directory) => directory.name(),
        }
    }

-
    pub(crate) fn from_entry(entry: &git2::TreeEntry) -> Result<Option<Self>, error::Entry> {
-
        let name = Label {
-
            label: entry.name().ok_or(error::Entry::Utf8Error)?.to_string(),
-
            hidden: false,
-
        };
+
    pub(crate) fn from_entry(
+
        entry: &git2::TreeEntry,
+
        path: PathBuf,
+
    ) -> Result<Option<Self>, error::Entry> {
+
        let name = entry.name().ok_or(error::Entry::Utf8Error)?.to_string();
        let id = entry.id().into();
        Ok(entry.kind().and_then(|kind| match kind {
-
            git2::ObjectType::Tree => Some(Self::Directory(Directory { name, id })),
-
            git2::ObjectType::Blob => Some(Self::File(File { name, id })),
+
            git2::ObjectType::Tree => Some(Self::Directory(Directory::new(name, path, id))),
+
            git2::ObjectType::Blob => Some(Self::File(File::new(name, path, id))),
            _ => None,
        }))
    }
@@ -204,21 +221,63 @@ impl Entry {
/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directory {
-
    pub(crate) name: Label,
-
    pub(crate) id: Oid,
+
    /// The name of the directoy.
+
    name: String,
+
    /// The relative path of the directory, not including the `name`,
+
    /// in respect to the root of the git repository.
+
    prefix: PathBuf,
+
    /// The object identifier of the git tree of this directory.
+
    id: Oid,
}

impl Directory {
-
    /// Get the name of the current `Directory`.
-
    pub fn name(&self) -> &Label {
-
        &self.name
+
    /// Creates a directory given its `id`.
+
    ///
+
    /// The `name` and `prefix` are both set to be empty.
+
    ///
+
    /// The `id` must point to a `git` tree.
+
    pub(crate) fn root(id: Oid) -> Self {
+
        Self::new("".to_string(), PathBuf::new(), id)
    }

    /// Creates a directory given its `name` and `id`.
    ///
+
    /// The `path` must be the prefix location of the directory, and
+
    /// so should not end in `name`.
+
    ///
    /// The `id` must point to a `git` tree.
-
    pub fn new(name: Label, id: Oid) -> Self {
-
        Self { name, id }
+
    pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+
        debug_assert!(
+
            name.is_empty() || !prefix.ends_with(&name),
+
            "prefix = {:?}, name = {}",
+
            prefix,
+
            name
+
        );
+
        Self { name, prefix, id }
+
    }
+

+
    /// Get the name of the current `Directory`.
+
    pub fn name(&self) -> &String {
+
        &self.name
+
    }
+

+
    /// The object identifier of this `File`.
+
    pub fn id(&self) -> Oid {
+
        self.id
+
    }
+

+
    /// Return the exact path for this `Directory`, including the `name` of the
+
    /// directory itself.
+
    ///
+
    /// The path is relative to the git repository root.
+
    pub fn path(&self) -> PathBuf {
+
        self.prefix.join(&self.name)
+
    }
+

+
    /// Return the [`Path`] where this `Directory` is located, relative to the
+
    /// git repository root.
+
    pub fn location(&self) -> &Path {
+
        &self.prefix
    }

    /// Return the [`Entries`] for this `Directory`'s `Oid`.
@@ -238,15 +297,15 @@ impl Directory {
        let mut error = None;

        // Walks only the first level of entries.
-
        tree.walk(git2::TreeWalkMode::PreOrder, |_s, entry| {
-
            match Entry::from_entry(entry) {
+
        tree.walk(git2::TreeWalkMode::PreOrder, |path, entry| {
+
            match Entry::from_entry(entry, Path::new(path).to_path_buf()) {
                Ok(Some(entry)) => match entry {
                    Entry::File(_) => {
-
                        entries.insert(entry.label().clone(), entry);
+
                        entries.insert(entry.name().clone(), entry);
                        git2::TreeWalkResult::Ok
                    },
                    Entry::Directory(_) => {
-
                        entries.insert(entry.label().clone(), entry);
+
                        entries.insert(entry.name().clone(), entry);
                        // Skip nested directories
                        git2::TreeWalkResult::Skip
                    },
@@ -266,45 +325,52 @@ impl Directory {
    }

    /// Find the [`Entry`] found at `path`, if it exists.
-
    pub fn find_entry(
+
    pub fn find_entry<P>(
        &self,
-
        path: &path::Path,
+
        path: &P,
        repo: &Repository,
-
    ) -> Result<Option<Entry>, crate::git::Error> {
+
    ) -> Result<Option<Entry>, crate::git::Error>
+
    where
+
        P: AsRef<Path>,
+
    {
        // Search the path in git2 tree.
        let git2_tree = repo.git2_repo().find_tree(self.id.into())?;
        let entry = git2_tree
-
            .get_path(path)
+
            .get_path(path.as_ref())
            .map(Some)
            .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(None))?;

        Ok(entry
-
            .and_then(|entry| Entry::from_entry(&entry).transpose())
+
            .and_then(|entry| Entry::from_entry(&entry, path.as_ref().to_path_buf()).transpose())
            .transpose()
            .unwrap())
    }

    /// Find the `Oid`, for a [`File`], found at `path`, if it exists.
-
    pub fn find_file(
+
    pub fn find_file<P>(
        &self,
-
        path: Path,
+
        path: &P,
        repo: &Repository,
-
    ) -> Result<Option<Oid>, crate::git::Error> {
-
        let path_buf: std::path::PathBuf = (&path).into();
-
        Ok(match self.find_entry(path_buf.as_path(), repo)? {
+
    ) -> Result<Option<Oid>, crate::git::Error>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        Ok(match self.find_entry(path, repo)? {
            Some(Entry::File(f)) => Some(f.id),
            _ => None,
        })
    }

    /// Find the `Directory` found at `path`, if it exists.
-
    pub fn find_directory(
+
    pub fn find_directory<P>(
        &self,
-
        path: Path,
+
        path: P,
        repo: &Repository,
-
    ) -> Result<Option<Self>, crate::git::Error> {
-
        let path_buf: std::path::PathBuf = (&path).into();
-
        Ok(match self.find_entry(path_buf.as_path(), repo)? {
+
    ) -> Result<Option<Self>, crate::git::Error>
+
    where
+
        P: AsRef<Path>,
+
    {
+
        Ok(match self.find_entry(&path, repo)? {
            Some(Entry::Directory(d)) => Some(d),
            _ => None,
        })
@@ -313,7 +379,7 @@ impl Directory {
    // TODO(fintan): This is going to be a bit trickier so going to leave it out for
    // now
    #[allow(dead_code)]
-
    fn fuzzy_find(_label: Label) -> Vec<Self> {
+
    fn fuzzy_find(_label: &Path) -> Vec<Self> {
        unimplemented!()
    }

deleted radicle-surf/src/file_system/error.rs
@@ -1,79 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! Errors that can occur within the file system logic.
-
//!
-
//! These errors occur due to [`Label`](super::path::Label) and
-
//! [`Path`](super::path::Path) parsing when using their respective `TryFrom`
-
//! instances.
-

-
use std::ffi::OsStr;
-
use thiserror::Error;
-

-
pub(crate) const EMPTY_PATH: Error = Error::Path(PathError::Empty);
-
pub(crate) const EMPTY_LABEL: Error = Error::Label(LabelError::Empty);
-

-
/// Build an [`Error::Label(LabelError::InvalidUTF8)`] from an
-
/// [`OsStr`](std::ffi::OsStr)
-
pub(crate) fn label_invalid_utf8(item: &OsStr) -> Error {
-
    Error::Label(LabelError::InvalidUTF8 {
-
        label: item.to_string_lossy().into(),
-
    })
-
}
-

-
/// Build an [`Error::Label(LabelError::ContainsSlash)`] from a [`str`]
-
pub(crate) fn label_has_slash(item: &str) -> Error {
-
    Error::Label(LabelError::ContainsSlash { label: item.into() })
-
}
-

-
/// Error type for all file system errors that can occur.
-
#[derive(Debug, Clone, PartialEq, Eq, Error)]
-
#[non_exhaustive]
-
pub enum Error {
-
    /// A `LabelError` specific error for parsing a
-
    /// [`Path`](super::path::Label).
-
    #[error(transparent)]
-
    Label(#[from] LabelError),
-
    /// A `PathError` specific error for parsing a [`Path`](super::path::Path).
-
    #[error(transparent)]
-
    Path(#[from] PathError),
-
}
-

-
/// Parse errors for when parsing a string to a [`Path`](super::path::Path).
-
#[derive(Debug, Clone, PartialEq, Eq, Error)]
-
#[non_exhaustive]
-
pub enum PathError {
-
    /// An error signifying that a [`Path`](super::path::Path) is empty.
-
    #[error("path is empty")]
-
    Empty,
-
}
-

-
/// Parse errors for when parsing a string to a [`Label`](super::path::Label).
-
#[derive(Debug, Clone, PartialEq, Eq, Error)]
-
#[non_exhaustive]
-
pub enum LabelError {
-
    /// An error signifying that a [`Label`](super::path::Label) is contains
-
    /// invalid UTF-8.
-
    #[error("label '{label}' contains invalid UTF-8")]
-
    InvalidUTF8 { label: String },
-
    /// An error signifying that a [`Label`](super::path::Label) contains a `/`.
-
    #[error("label '{label}' contains a slash")]
-
    ContainsSlash { label: String },
-
    /// An error signifying that a [`Label`](super::path::Label) is empty.
-
    #[error("label is empty")]
-
    Empty,
-
}
deleted radicle-surf/src/file_system/path.rs
@@ -1,453 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
use std::fmt::Write as _;
-

-
use crate::{file_system::error, nonempty::split_last};
-
use nonempty::NonEmpty;
-
use std::{convert::TryFrom, ffi::CString, fmt, ops::Deref, path, str::FromStr};
-

-
#[cfg(feature = "serialize")]
-
use serde::{Serialize, Serializer};
-

-
pub mod unsound;
-

-
/// `Label` is a special case of a `String` identifier for
-
/// [`Directory`](`crate::file_system::directory::Directory`) and
-
/// [`File`](`crate::file_system::directory::File`) names, and is used in
-
/// [`Path`] as the component parts of a path.
-
///
-
/// A `Label` should not be empty or contain `/`s. It is encouraged to use the
-
/// `TryFrom` instance to create a `Label`.
-
#[cfg_attr(feature = "serialize", derive(Serialize))]
-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Label {
-
    pub(crate) label: String,
-
    pub(crate) hidden: bool,
-
}
-

-
impl Deref for Label {
-
    type Target = String;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.label
-
    }
-
}
-

-
/// The label for the root directory.
-
pub const ROOT_LABEL: &str = "~";
-

-
impl Label {
-
    /// The root label for the root directory, i.e. `"~"`.
-
    ///
-
    /// Prefer creating a root [`Path`], by using
-
    /// [`Path::root`](struct.Path.html#method.root).
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    ///
-
    /// let root = Path::root();
-
    /// assert_eq!(*root.split_first().0, Label::root());
-
    /// ```
-
    pub fn root() -> Self {
-
        Label {
-
            label: ROOT_LABEL.into(),
-
            hidden: false,
-
        }
-
    }
-

-
    /// Check that the label is equivalent to [`Label::root`].
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::Label;
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let root = unsound::label::new("~");
-
    /// assert!(root.is_root());
-
    /// ```
-
    pub fn is_root(&self) -> bool {
-
        *self == Self::root()
-
    }
-
}
-

-
impl fmt::Display for Label {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.label)
-
    }
-
}
-

-
impl TryFrom<&str> for Label {
-
    type Error = error::Error;
-

-
    fn try_from(item: &str) -> Result<Self, Self::Error> {
-
        if item.is_empty() {
-
            Err(error::EMPTY_LABEL)
-
        } else if item.contains('/') {
-
            Err(error::label_has_slash(item))
-
        } else {
-
            Ok(Label {
-
                label: item.into(),
-
                hidden: false,
-
            })
-
        }
-
    }
-
}
-

-
impl FromStr for Label {
-
    type Err = error::Error;
-

-
    fn from_str(item: &str) -> Result<Self, Self::Err> {
-
        Label::try_from(item)
-
    }
-
}
-

-
/// A non-empty set of [`Label`]s to define a path to a directory or file.
-
///
-
/// `Path` tends to be used for insertion or find operations.
-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-
pub struct Path(pub NonEmpty<Label>);
-

-
impl fmt::Display for Path {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let (prefix, suffix) = self.clone().split_last();
-
        for p in prefix {
-
            if p.is_root() {
-
                continue;
-
            }
-
            write!(f, "{}/", p)?;
-
        }
-
        write!(f, "{}", suffix)
-
    }
-
}
-

-
impl TryFrom<&str> for Path {
-
    type Error = error::Error;
-

-
    fn try_from(item: &str) -> Result<Self, Self::Error> {
-
        let mut path = Vec::new();
-

-
        for label in item.trim_end_matches('/').split('/') {
-
            let l = Label::try_from(label)?;
-
            path.push(l);
-
        }
-

-
        NonEmpty::from_slice(&path)
-
            .ok_or(error::EMPTY_PATH)
-
            .map(Path)
-
    }
-
}
-

-
impl FromStr for Path {
-
    type Err = error::Error;
-

-
    fn from_str(item: &str) -> Result<Self, Self::Err> {
-
        Path::try_from(item)
-
    }
-
}
-

-
impl From<Path> for Vec<Label> {
-
    fn from(path: Path) -> Self {
-
        path.0.into()
-
    }
-
}
-

-
impl From<&Path> for path::PathBuf {
-
    fn from(path: &Path) -> Self {
-
        path.iter()
-
            .map(|label| label.as_str())
-
            .collect::<path::PathBuf>()
-
    }
-
}
-

-
impl git2::IntoCString for Path {
-
    fn into_c_string(self) -> Result<CString, git2::Error> {
-
        if self.is_root() {
-
            // the root pathsec is empty
-
            "".into_c_string()
-
        } else {
-
            // build the file path pathsec
-
            let path = self.0.tail;
-
            let mut pathspec = "".to_string();
-
            for p in path.iter() {
-
                // If we have a label such as 'faux\path' we need to double escape it for
-
                // `git2::DiffOptions::pathspec` to work properly. As far as we're aware this is
-
                // the only use of IntoCString for Path.
-
                let label = p.label.replace('\\', "\\\\");
-
                let _ = write!(pathspec, "{label}/");
-
            }
-
            let pathspec = pathspec.trim_end_matches('/');
-
            pathspec.into_c_string()
-
        }
-
    }
-
}
-

-
#[cfg(feature = "serialize")]
-
impl Serialize for Path {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(self.to_string().as_str())
-
    }
-
}
-

-
impl Path {
-
    /// Create a new `Path` with a single [`Label`].
-
    pub fn new(label: Label) -> Path {
-
        Path(NonEmpty::new(label))
-
    }
-

-
    /// The root path is a `Path` made up of the single root label (see:
-
    /// [`Label::root`](#method.root).
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    ///
-
    /// let root = Path::root();
-
    /// assert_eq!(*root.split_first().0, Label::root());
-
    /// ```
-
    pub fn root() -> Self {
-
        Path(NonEmpty::new(Label::root()))
-
    }
-

-
    /// Check that this is the root path.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::Path;
-
    /// use radicle_surf::file_system::unsound;
-
    /// use std::convert::TryFrom;
-
    ///
-
    /// let root = Path::root();
-
    /// let not_root = unsound::path::new("src/lib.rs");
-
    ///
-
    /// assert!(root.is_root());
-
    /// assert!(!not_root.is_root());
-
    /// ```
-
    pub fn is_root(&self) -> bool {
-
        *self == Self::root()
-
    }
-

-
    /// Append two `Path`s together.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::Path;
-
    /// use radicle_surf::file_system::unsound;
-
    /// use std::convert::TryFrom;
-
    ///
-
    /// let mut path1 = unsound::path::new("foo/bar");
-
    /// let path2 = unsound::path::new("baz/quux");
-
    /// path1.append(path2);
-
    /// let expected = unsound::path::new("foo/bar/baz/quux");
-
    /// assert_eq!(path1, expected);
-
    /// ```
-
    pub fn append(&mut self, path: Self) {
-
        let mut other = path.0.into();
-
        self.0.append(&mut other)
-
    }
-

-
    /// Push a new [`Label`] onto the `Path`.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut root = Path::root();
-
    /// root.push(unsound::label::new("src"));
-
    /// root.push(unsound::label::new("lib.rs"));
-
    ///
-
    /// assert_eq!(root, unsound::path::new("~/src/lib.rs"));
-
    /// ```
-
    pub fn push(&mut self, label: Label) {
-
        self.0.push(label)
-
    }
-

-
    /// Pop the [`Label`] from the end of the tail.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let mut root = Path::root();
-
    /// root.push(unsound::label::new("src"));
-
    /// root.push(unsound::label::new("lib.rs"));
-
    ///
-
    /// assert_eq!(root.pop(), Some(unsound::label::new("lib.rs")));
-
    /// ```
-
    pub fn pop(&mut self) -> Option<Label> {
-
        self.0.pop()
-
    }
-

-
    /// Iterator over the [`Label`]s in the `Path`.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = unsound::path::new("~/src/lib.rs");
-
    /// let mut path_iter = path.iter();
-
    ///
-
    /// assert_eq!(path_iter.next(), Some(&Label::root()));
-
    /// assert_eq!(path_iter.next(), Some(&unsound::label::new("src")));
-
    /// assert_eq!(path_iter.next(), Some(&unsound::label::new("lib.rs")));
-
    /// ```
-
    pub fn iter(&self) -> impl Iterator<Item = &Label> {
-
        self.0.iter()
-
    }
-

-
    /// Get the first [`Label`] in the `Path` and the rest of the [`Label`]s
-
    /// after it.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = unsound::path::new("~/src/lib.rs");
-
    ///
-
    /// assert_eq!(
-
    ///     path.split_first(),
-
    ///     (&Label::root(), &[unsound::label::new("src"), unsound::label::new("lib.rs")][..])
-
    /// );
-
    /// ```
-
    pub fn split_first(&self) -> (&Label, &[Label]) {
-
        self.0.split_first()
-
    }
-

-
    /// Get the prefix of the [`Label`]s and the last [`Label`].
-
    ///
-
    /// This is useful when the prefix is a directory path and the last label is
-
    /// a file name.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = unsound::path::new("~/src/lib.rs");
-
    /// assert_eq!(path.split_last(), (vec![Label::root(), unsound::label::new("src")], unsound::label::new("lib.rs")));
-
    /// ```
-
    ///
-
    /// ```
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = unsound::path::new("foo/bar/baz");
-
    /// assert_eq!(
-
    ///     path.split_last(),
-
    ///     (vec![unsound::label::new("foo"), unsound::label::new("bar")], unsound::label::new("baz"))
-
    /// );
-
    /// ```
-
    pub fn split_last(self) -> (Vec<Label>, Label) {
-
        split_last(self.0)
-
    }
-

-
    /// Construct a `Path` given at least one [`Label`] followed by 0 or more
-
    /// [`Label`]s.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use nonempty::NonEmpty;
-
    /// use radicle_surf::file_system::{Path, Label};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = Path::from_labels(
-
    ///     Label::root(),
-
    ///     &[unsound::label::new("foo"), unsound::label::new("bar"), unsound::label::new("baz.rs")]
-
    /// );
-
    ///
-
    /// let mut expected = Path::root();
-
    /// expected.push(unsound::label::new("foo"));
-
    /// expected.push(unsound::label::new("bar"));
-
    /// expected.push(unsound::label::new("baz.rs"));
-
    ///
-
    /// assert_eq!(path, expected);
-
    /// let path_vec: Vec<Label> = path.0.into();
-
    /// assert_eq!(
-
    ///     path_vec,
-
    ///     vec![Label::root(), unsound::label::new("foo"), unsound::label::new("bar"),
-
    ///     unsound::label::new("baz.rs")]
-
    /// );
-
    /// ```
-
    pub fn from_labels(root: Label, labels: &[Label]) -> Path {
-
        Path((root, labels.to_vec()).into())
-
    }
-

-
    /// Construct a `Path` using [`Label::root`](#method.root) as the head of
-
    /// the `Path.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use nonempty::NonEmpty;
-
    /// use radicle_surf::file_system::{Label, Path};
-
    /// use radicle_surf::file_system::unsound;
-
    ///
-
    /// let path = Path::with_root(
-
    ///     &[unsound::label::new("foo"), unsound::label::new("bar"), unsound::label::new("baz.rs")]
-
    /// );
-
    ///
-
    /// let mut expected = Path::root();
-
    /// expected.push(unsound::label::new("foo"));
-
    /// expected.push(unsound::label::new("bar"));
-
    /// expected.push(unsound::label::new("baz.rs"));
-
    ///
-
    /// assert_eq!(path, expected);
-
    /// let path_vec: Vec<Label> = path.0.into();
-
    /// assert_eq!(
-
    ///     path_vec,
-
    ///     vec![Label::root(), unsound::label::new("foo"), unsound::label::new("bar"),
-
    ///     unsound::label::new("baz.rs")]
-
    /// );
-
    /// ```
-
    pub fn with_root(labels: &[Label]) -> Path {
-
        Path::from_labels(Label::root(), labels)
-
    }
-
}
-

-
impl TryFrom<path::PathBuf> for Path {
-
    type Error = error::Error;
-

-
    fn try_from(path_buf: path::PathBuf) -> Result<Self, Self::Error> {
-
        let mut path = Path::root();
-
        for p in path_buf.iter() {
-
            let p = p.to_str().ok_or_else(|| error::label_invalid_utf8(p))?;
-
            let l = Label::try_from(p)?;
-
            path.push(l);
-
        }
-

-
        Ok(path)
-
    }
-
}
deleted radicle-surf/src/file_system/path/unsound.rs
@@ -1,62 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
//! In Irish slang there exists the term "sound". One is a "sound" person if
-
//! they are nice and you can rely on them. This module is the anithesis of
-
//! being "sound", you might say it is "unsound".
-
//!
-
//! The aim of this module is to make testing easier. During test time, _we
-
//! know_ that a string is going to be non-empty because we are using the
-
//! literal `"sound_label"`. The same for knowing that the form
-
//! `"what/a/sound/bunch"` is a valid path.
-
//!
-
//! On the other hand, if we do not control the data coming in we should use the
-
//! more "sound" method of the [`std::convert::TryFrom`] instance for
-
//! [`crate::file_system::Label`] and [`crate::file_system::Path`]
-
//! to ensure we have valid data to use for further operations.
-

-
pub mod path {
-
    //! Unsound creation of [`Path`]s.
-

-
    use crate::file_system::path::Path;
-
    use std::convert::TryFrom;
-

-
    /// **NB**: Use with caution!
-
    ///
-
    /// Calls `try_from` on the input and expects it to not fail.
-
    ///
-
    /// Used for testing and playground purposes.
-
    pub fn new(path: &str) -> Path {
-
        Path::try_from(path).expect("unsafe_path: Failed to parse path")
-
    }
-
}
-

-
pub mod label {
-
    //! Unsound creation of [`Label`]s.
-

-
    use crate::file_system::path::Label;
-
    use std::convert::TryFrom;
-

-
    /// **NB**: Use with caution!
-
    ///
-
    /// Calls `try_from` on the input and expects it to not fail.
-
    ///
-
    /// Used for testing and playground purposes.
-
    pub fn new(path: &str) -> Label {
-
        Label::try_from(path).expect("unsafe_path: Failed to parse label")
-
    }
-
}
modified radicle-surf/src/git/commit.rs
@@ -17,7 +17,7 @@

use std::{convert::TryFrom, str};

-
use radicle_git_ext::Oid;
+
use git_ext::Oid;
use thiserror::Error;

#[cfg(feature = "serialize")]
modified radicle-surf/src/git/history.rs
@@ -15,11 +15,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

-
use crate::{
-
    file_system,
-
    git::{Commit, Error, Repository, ToCommit},
+
use std::{
+
    convert::TryFrom,
+
    path::{Path, PathBuf},
};
-
use std::convert::TryFrom;
+

+
use crate::git::{Commit, Error, Repository, ToCommit};

/// An iterator that produces the history of commits for a given `head`,
/// in the `repo`.
@@ -32,7 +33,7 @@ pub struct History<'a> {

/// Internal implementation, subject to refactoring.
enum FilterBy {
-
    File { path: file_system::Path },
+
    File { path: PathBuf },
}

impl<'a> History<'a> {
@@ -61,8 +62,13 @@ impl<'a> History<'a> {
    ///
    /// Note that it is possible that a filtered History becomes empty,
    /// even though calling `.head()` still returns the original head.
-
    pub fn by_path(mut self, path: file_system::Path) -> Self {
-
        self.filter_by = Some(FilterBy::File { path });
+
    pub fn by_path<P>(mut self, path: P) -> Self
+
    where
+
        P: AsRef<Path>,
+
    {
+
        self.filter_by = Some(FilterBy::File {
+
            path: path.as_ref().to_path_buf(),
+
        });
        self
    }
}
modified radicle-surf/src/git/repo.rs
@@ -15,7 +15,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

-
use std::{collections::BTreeSet, convert::TryFrom, path::PathBuf, str};
+
use std::{
+
    collections::BTreeSet,
+
    convert::TryFrom,
+
    path::{Path, PathBuf},
+
    str,
+
};

use git_ref_format::{refspec::QualifiedPattern, Qualified};
use radicle_git_ext::Oid;
@@ -23,8 +28,7 @@ use thiserror::Error;

use crate::{
    diff::{self, *},
-
    file_system,
-
    file_system::{directory::FileContent, Directory, Label},
+
    file_system::{directory::FileContent, Directory},
    git::{
        commit,
        glob,
@@ -58,9 +62,6 @@ pub enum Error {
    /// An error that comes from performing a *diff* operations.
    #[error(transparent)]
    Diff(#[from] diff::git::error::Diff),
-
    /// An error that comes from performing a [`crate::file_system`] operation.
-
    #[error(transparent)]
-
    FileSystem(#[from] file_system::Error),
    /// A wrapper around the generic [`git2::Error`].
    #[error(transparent)]
    Git(#[from] git2::Error),
@@ -72,7 +73,7 @@ pub enum Error {
    NotQualified(String),
    /// The requested file was not found.
    #[error("path not found for: {0}")]
-
    PathNotFound(file_system::Path),
+
    PathNotFound(PathBuf),
    #[error(transparent)]
    RefFormat(#[from] git_ref_format::Error),
    #[error(transparent)]
@@ -187,19 +188,16 @@ impl Repository {
            .map_err(|err| Error::ToCommit(err.into()))?;
        let git2_commit = self.inner.find_commit((commit.id).into())?;
        let tree = git2_commit.as_object().peel_to_tree()?;
-
        Ok(Directory {
-
            name: Label::root(),
-
            id: tree.id().into(),
-
        })
+
        Ok(Directory::root(tree.id().into()))
    }

    /// Returns the last commit, if exists, for a `path` in the history of
    /// `rev`.
-
    pub fn last_commit<C: ToCommit>(
-
        &self,
-
        path: file_system::Path,
-
        rev: C,
-
    ) -> Result<Option<Commit>, Error> {
+
    pub fn last_commit<P, C>(&self, path: P, rev: C) -> Result<Option<Commit>, Error>
+
    where
+
        P: AsRef<Path>,
+
        C: ToCommit,
+
    {
        let history = self.history(rev)?;
        history.by_path(path).next().transpose()
    }
@@ -239,17 +237,20 @@ impl Repository {
    }

    /// Retrieves the file with `path` in this commit.
-
    pub fn get_commit_file<R: Revision>(
-
        &self,
-
        rev: &R,
-
        path: file_system::Path,
-
    ) -> Result<FileContent, crate::git::Error> {
+
    pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, crate::git::Error>
+
    where
+
        P: AsRef<Path>,
+
        R: Revision,
+
    {
+
        let path = path.as_ref();
        let id = self.object_id(rev)?;
        let commit = self.get_git2_commit(id)?;
        let tree = commit.tree()?;
-
        let entry = tree.get_path(PathBuf::from(&path).as_ref())?;
+
        let entry = tree.get_path(path)?;
        let object = entry.to_object(self.git2_repo())?;
-
        let blob = object.into_blob().map_err(|_| Error::PathNotFound(path))?;
+
        let blob = object
+
            .into_blob()
+
            .map_err(|_| Error::PathNotFound(path.to_path_buf()))?;
        Ok(FileContent::new(blob))
    }

@@ -355,16 +356,19 @@ impl Repository {
        Ok(other == git2_oid || is_descendant)
    }

-
    pub(crate) fn diff_commit_and_parents(
+
    pub(crate) fn diff_commit_and_parents<P>(
        &self,
-
        path: &file_system::Path,
+
        path: &P,
        commit: &git2::Commit,
-
    ) -> Result<Option<file_system::Path>, Error> {
+
    ) -> Result<Option<PathBuf>, Error>
+
    where
+
        P: AsRef<Path>,
+
    {
        let mut parents = commit.parents();

-
        let diff = self.diff_commits(Some(path), parents.next().as_ref(), commit)?;
+
        let diff = self.diff_commits(Some(path.as_ref()), parents.next().as_ref(), commit)?;
        if let Some(_delta) = diff.deltas().next() {
-
            Ok(Some(path.clone()))
+
            Ok(Some(path.as_ref().to_path_buf()))
        } else {
            Ok(None)
        }
@@ -372,7 +376,7 @@ impl Repository {

    fn diff_commits(
        &self,
-
        path: Option<&file_system::Path>,
+
        path: Option<&Path>,
        from: Option<&git2::Commit>,
        to: &git2::Commit,
    ) -> Result<git2::Diff, Error> {
modified radicle-surf/src/lib.rs
@@ -99,6 +99,3 @@ pub use revision::Revision;
pub mod syntax;
#[cfg(feature = "syntax")]
pub use syntax::SYNTAX_SET;
-

-
// Private modules
-
mod nonempty;
deleted radicle-surf/src/nonempty.rs
@@ -1,33 +0,0 @@
-
// This file is part of radicle-surf
-
// <https://github.com/radicle-dev/radicle-surf>
-
//
-
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
-
//
-
// This program is free software: you can redistribute it and/or modify
-
// it under the terms of the GNU General Public License version 3 or
-
// later as published by the Free Software Foundation.
-
//
-
// This program is distributed in the hope that it will be useful,
-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-
// GNU General Public License for more details.
-
//
-
// You should have received a copy of the GNU General Public License
-
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-

-
use nonempty::NonEmpty;
-

-
pub fn split_last<T>(non_empty: NonEmpty<T>) -> (Vec<T>, T)
-
where
-
    T: Eq,
-
{
-
    let (head, mut tail) = non_empty.into();
-
    let last = tail.pop();
-
    match last {
-
        None => (vec![], head),
-
        Some(last) => {
-
            tail.insert(0, head);
-
            (tail, last)
-
        },
-
    }
-
}
modified radicle-surf/src/object.rs
@@ -18,6 +18,8 @@
//! Common definitions for git objects (blob and tree).
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.

+
use std::path::PathBuf;
+

#[cfg(feature = "serialize")]
use serde::{
    ser::{SerializeStruct as _, Serializer},
@@ -30,11 +32,7 @@ pub use blob::{Blob, BlobContent};
pub mod tree;
pub use tree::{tree, Tree, TreeEntry};

-
use crate::{
-
    commit,
-
    file_system::{self, directory},
-
    git,
-
};
+
use crate::{commit, file_system::directory, git};

/// Git object types.
///
@@ -91,15 +89,11 @@ pub enum Error {
    #[error(transparent)]
    Directory(#[from] directory::error::Directory),

-
    /// An error occurred during a file system operation.
-
    #[error(transparent)]
-
    FileSystem(#[from] file_system::Error),
-

    /// An error occurred during a git operation.
    #[error(transparent)]
    Git(#[from] git::Error),

    /// Trying to find a file path which could not be found.
    #[error("the path '{0}' was not found")]
-
    PathNotFound(file_system::Path),
+
    PathNotFound(PathBuf),
}
modified radicle-surf/src/object/blob.rs
@@ -18,7 +18,10 @@
//! Represents git object type 'blob', i.e. actual file contents.
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.

-
use std::str::{self, FromStr as _};
+
use std::{
+
    path::{Path, PathBuf},
+
    str,
+
};

#[cfg(feature = "serialize")]
use serde::{
@@ -28,7 +31,6 @@ use serde::{

use crate::{
    commit,
-
    file_system,
    git::Repository,
    object::{Error, Info, ObjectType},
    revision::Revision,
@@ -44,7 +46,7 @@ pub struct Blob {
    /// Extra info for the file.
    pub info: Info,
    /// Absolute path to the object from the root of the repo.
-
    pub path: String,
+
    pub path: PathBuf,
}

impl Blob {
@@ -114,38 +116,42 @@ impl Serialize for BlobContent {
///
/// Will return [`Error`] if the project doesn't exist or a surf interaction
/// fails.
-
pub fn blob(
-
    repo: &Repository,
-
    maybe_revision: Option<Revision>,
-
    path: &str,
-
) -> Result<Blob, Error> {
+
pub fn blob<P>(repo: &Repository, maybe_revision: Option<Revision>, path: &P) -> Result<Blob, Error>
+
where
+
    P: AsRef<Path>,
+
{
    make_blob(repo, maybe_revision, path, content)
}

-
fn make_blob<C>(
+
fn make_blob<P, C>(
    repo: &Repository,
    maybe_revision: Option<Revision>,
-
    path: &str,
+
    path: &P,
    content: C,
) -> Result<Blob, Error>
where
+
    P: AsRef<Path>,
    C: FnOnce(&[u8]) -> BlobContent,
{
+
    let path = path.as_ref();
    let revision = maybe_revision.unwrap();
    let root = repo.root_dir(&revision)?;
-
    let p = file_system::Path::from_str(path)?;

    let file = root
-
        .find_file(p.clone(), repo)?
-
        .ok_or_else(|| Error::PathNotFound(p.clone()))?;
-

-
    let mut commit_path = file_system::Path::root();
-
    commit_path.append(p.clone());
+
        .find_file(&path, repo)?
+
        .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?;

    let last_commit = repo
-
        .last_commit(commit_path, &revision)?
+
        .last_commit(path, &revision)?
        .map(|c| commit::Header::from(&c));
-
    let (_rest, last) = p.split_last();
+
    // TODO: fuck this
+
    let name = path
+
        .file_name()
+
        .unwrap()
+
        .to_os_string()
+
        .into_string()
+
        .ok()
+
        .unwrap();

    let file_content = repo.file_content(file)?;
    let content = content(file_content.as_bytes());
@@ -153,11 +159,11 @@ where
    Ok(Blob {
        content,
        info: Info {
-
            name: last.to_string(),
+
            name,
            object_type: ObjectType::Blob,
            last_commit,
        },
-
        path: path.to_string(),
+
        path: path.to_path_buf(),
    })
}

modified radicle-surf/src/object/tree.rs
@@ -18,7 +18,7 @@
//! Represents git object type 'tree', i.e. like directory entries in Unix.
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.

-
use std::str::FromStr as _;
+
use std::path::{Path, PathBuf};

use git_ref_format::refname;
#[cfg(feature = "serialize")]
@@ -29,7 +29,7 @@ use serde::{

use crate::{
    commit,
-
    file_system::{self, directory},
+
    file_system::directory,
    git::Repository,
    object::{Error, Info, ObjectType},
    revision::Revision,
@@ -38,7 +38,7 @@ use crate::{
/// Result of a directory listing, carries other trees and blobs.
pub struct Tree {
    /// Absolute path to the tree object from the repo root.
-
    pub path: String,
+
    pub path: PathBuf,
    /// Entries listed in that tree result.
    pub entries: Vec<TreeEntry>,
    /// Extra info for the tree object.
@@ -65,7 +65,7 @@ pub struct TreeEntry {
    /// Extra info for the entry.
    pub info: Info,
    /// Absolute path to the object from the root of the repo.
-
    pub path: String,
+
    pub path: PathBuf,
}

#[cfg(feature = "serialize")]
@@ -86,12 +86,15 @@ impl Serialize for TreeEntry {
/// # Errors
///
/// Will return [`Error`] if any of the surf interactions fail.
-
pub fn tree(
+
pub fn tree<P>(
    repo: &Repository,
    maybe_revision: Option<Revision>,
-
    maybe_prefix: Option<String>,
-
) -> Result<Tree, Error> {
-
    let prefix = maybe_prefix.unwrap_or_default();
+
    maybe_prefix: Option<&P>,
+
) -> Result<Tree, Error>
+
where
+
    P: AsRef<Path>,
+
{
+
    let maybe_prefix = maybe_prefix.map(|p| p.as_ref());
    let rev = match maybe_revision {
        Some(r) => r,
        None => Revision::Branch {
@@ -100,51 +103,42 @@ pub fn tree(
        },
    };

-
    let path = if prefix == "/" || prefix.is_empty() {
-
        file_system::Path::root()
-
    } else {
-
        file_system::Path::from_str(&prefix)?
+
    let prefix_dir = match maybe_prefix {
+
        None => repo.root_dir(&rev)?,
+
        Some(path) => repo
+
            .root_dir(&rev)?
+
            .find_directory(path, repo)?
+
            .ok_or_else(|| Error::PathNotFound(path.to_path_buf()))?,
    };

-
    let root_dir = repo.root_dir(&rev)?;
-
    let prefix_dir = if path.is_root() {
-
        root_dir
-
    } else {
-
        root_dir
-
            .find_directory(path.clone(), repo)?
-
            .ok_or_else(|| Error::PathNotFound(path.clone()))?
-
    };
-

-
    let mut entries =
-
        prefix_dir
-
            .entries(repo)?
-
            .iter()
-
            .fold(Vec::new(), |mut entries, (label, entry)| {
-
                let entry_path = if path.is_root() {
-
                    file_system::Path::new(label.clone())
-
                } else {
-
                    let mut p = path.clone();
-
                    p.push(label.clone());
-
                    p
-
                };
-
                let mut commit_path = file_system::Path::root();
-
                commit_path.append(entry_path.clone());
-

-
                let info = Info {
-
                    name: label.to_string(),
-
                    object_type: match entry {
-
                        directory::Entry::Directory(_) => ObjectType::Tree,
-
                        directory::Entry::File { .. } => ObjectType::Blob,
-
                    },
-
                    last_commit: None,
-
                };
-

-
                entries.push(TreeEntry {
-
                    info,
-
                    path: entry_path.to_string(),
-
                });
-
                entries
+
    let mut entries = prefix_dir
+
        .entries(repo)?
+
        .entries()
+
        .fold(Vec::new(), |mut entries, entry| {
+
            let entry_path = match maybe_prefix {
+
                Some(path) => {
+
                    let mut path = path.to_path_buf();
+
                    path.push(entry.name());
+
                    path
+
                },
+
                None => PathBuf::new(),
+
            };
+

+
            let info = Info {
+
                name: entry.name().clone(),
+
                object_type: match entry {
+
                    directory::Entry::Directory(_) => ObjectType::Tree,
+
                    directory::Entry::File { .. } => ObjectType::Blob,
+
                },
+
                last_commit: None,
+
            };
+

+
            entries.push(TreeEntry {
+
                info,
+
                path: entry_path,
            });
+
            entries
+
        });

    // We want to ensure that in the response Tree entries come first. `Ord` being
    // derived on the enum ensures Variant declaration order.
@@ -152,17 +146,15 @@ pub fn tree(
    // https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
    entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));

-
    let last_commit = if path.is_root() {
+
    let last_commit = if maybe_prefix.is_none() {
        let history = repo.history(&rev)?;
        Some(commit::Header::from(history.head()))
    } else {
        None
    };
-
    let name = if path.is_root() {
-
        "".into()
-
    } else {
-
        let (_first, last) = path.split_last();
-
        last.to_string()
+
    let name = match maybe_prefix {
+
        None => "".into(),
+
        Some(path) => path.file_name().unwrap().to_str().unwrap().to_string(),
    };
    let info = Info {
        name,
@@ -171,7 +163,7 @@ pub fn tree(
    };

    Ok(Tree {
-
        path: prefix,
+
        path: maybe_prefix.map_or(PathBuf::new(), |path| path.to_path_buf()),
        entries,
        info,
    })
modified radicle-surf/t/src/file_system.rs
@@ -4,31 +4,6 @@
//! Unit tests for radicle_surf::file_system

#[cfg(test)]
-
mod path {
-
    use radicle_surf::file_system::unsound;
-

-
    #[test]
-
    fn split_last_root_and_foo() {
-
        let path = unsound::path::new("foo");
-
        assert_eq!(path.split_last(), (vec![], unsound::label::new("foo")));
-
    }
-

-
    #[test]
-
    fn split_last_same_labels() {
-
        // An interesting case for when first == last, but doesn't imply a singleton
-
        // Path.
-
        let path = unsound::path::new("foo/bar/foo");
-
        assert_eq!(
-
            path.split_last(),
-
            (
-
                vec![unsound::label::new("foo"), unsound::label::new("bar")],
-
                unsound::label::new("foo")
-
            )
-
        );
-
    }
-
}
-

-
#[cfg(test)]
mod directory {
    use git_ref_format::refname;
    use radicle_surf::{
modified radicle-surf/t/src/git.rs
@@ -7,7 +7,7 @@
use radicle_surf::git::{Author, Commit};
use radicle_surf::{
    diff::*,
-
    file_system::{directory, unsound, Path},
+
    file_system::directory,
    git::{Branch, Error, Glob, Oid, Repository},
};

@@ -246,7 +246,10 @@ mod last_commit {
    use git_ref_format::refname;

    use super::*;
-
    use std::str::FromStr;
+
    use std::{
+
        path::{Path, PathBuf},
+
        str::FromStr,
+
    };

    #[test]
    fn readme_missing_and_memory() {
@@ -257,10 +260,7 @@ mod last_commit {

        // memory.rs is commited later so it should not exist here.
        let memory_last_commit_oid = repo
-
            .last_commit(
-
                Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]),
-
                oid,
-
            )
+
            .last_commit(Path::new("src/memory.rs"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -268,7 +268,7 @@ mod last_commit {

        // README.md exists in this commit.
        let readme_last_commit = repo
-
            .last_commit(Path::with_root(&[unsound::label::new("README.md")]), oid)
+
            .last_commit(Path::new("README.md"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -286,7 +286,7 @@ mod last_commit {
        let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();

        let folder_svelte = repo
-
            .last_commit(unsound::path::new("~/examples/Folder.svelte"), oid)
+
            .last_commit(Path::new("examples/Folder.svelte"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -305,7 +305,7 @@ mod last_commit {

        let nested_directory_tree_commit_id = repo
            .last_commit(
-
                unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"),
+
                Path::new("this/is/a/really/deeply/nested/directory/tree"),
                oid,
            )
            .expect("Failed to get last commit")
@@ -327,13 +327,13 @@ mod last_commit {
        let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();

        let backslash_commit_id = repo
-
            .last_commit(unsound::path::new("~/special/faux\\path"), oid)
+
            .last_commit(Path::new(r"special/faux\\path"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
        assert_eq!(backslash_commit_id, Some(expected_commit_id));

        let ogre_commit_id = repo
-
            .last_commit(unsound::path::new("~/special/👹👹👹"), oid)
+
            .last_commit(Path::new("special/👹👹👹"), oid)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);
        assert_eq!(ogre_commit_id, Some(expected_commit_id));
@@ -345,7 +345,7 @@ mod last_commit {
            .expect("Could not retrieve ./data/git-platinum as git repository");
        let rev = Branch::local(refname!("master"));
        let root_last_commit_id = repo
-
            .last_commit(Path::root(), &rev)
+
            .last_commit(&PathBuf::new(), &rev)
            .expect("Failed to get last commit")
            .map(|commit| commit.id);

@@ -362,7 +362,7 @@ mod last_commit {
        let repo = Repository::open(GIT_PLATINUM)
            .expect("Could not retrieve ./data/git-platinum as git repository");
        let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
-
        let file_commit = history.by_path(unsound::path::new("~/bin/cat")).next();
+
        let file_commit = history.by_path(Path::new("bin/cat")).next();
        assert!(file_commit.is_some());
        println!("file commit: {:?}", &file_commit);
    }
@@ -373,7 +373,7 @@ mod diff {
    use super::*;
    use git_ref_format::refname;
    use pretty_assertions::assert_eq;
-
    use std::str::FromStr;
+
    use std::{path::Path, str::FromStr};

    #[test]
    fn test_initial_diff() -> Result<(), Error> {
@@ -386,7 +386,7 @@ mod diff {

        let expected_diff = Diff {
            created: vec![CreateFile {
-
                path: Path::with_root(&[unsound::label::new("README.md")]),
+
                path: Path::new("README.md").to_path_buf(),
                diff: FileDiff::Plain {
                    hunks: vec![Hunk {
                        header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
@@ -434,7 +434,7 @@ mod diff {
                moved: vec![],
                copied: vec![],
                modified: vec![ModifiedFile {
-
                    path: Path::with_root(&[unsound::label::new("README.md")]),
+
                    path: Path::new("README.md").to_path_buf(),
                    diff: FileDiff::Plain {
                        hunks: vec![Hunk {
                            header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
@@ -474,16 +474,16 @@ mod diff {
        assert_eq!(diff.moved.len(), 1);
        assert_eq!(diff.modified.len(), 2);
        for c in diff.created.iter() {
-
            println!("created: {}", &c.path);
+
            println!("created: {:?}", &c.path);
        }
        for d in diff.deleted.iter() {
-
            println!("deleted: {}", &d.path);
+
            println!("deleted: {:?}", &d.path);
        }
        for m in diff.moved.iter() {
-
            println!("moved: {} -> {}", &m.old_path, &m.new_path);
+
            println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
        }
        for m in diff.modified.iter() {
-
            println!("modified: {}", &m.path);
+
            println!("modified: {:?}", &m.path);
        }
        Ok(())
    }
@@ -717,6 +717,8 @@ mod reference {
}

mod code_browsing {
+
    use std::path::Path;
+

    use super::*;

    use git_ref_format::refname;
@@ -740,7 +742,7 @@ mod code_browsing {
                repo,
                (0, 0),
                &mut |(count, indent_level), entry| {
-
                    println!("> {}{}", " ".repeat(indent_level * 4), entry.label());
+
                    println!("> {}{}", " ".repeat(indent_level * 4), entry.name());
                    match entry {
                        directory::Entry::File(_) => Ok((count + 1, indent_level)),
                        directory::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
@@ -775,11 +777,11 @@ mod code_browsing {
    fn test_file_history() {
        let repo = Repository::open(GIT_PLATINUM).unwrap();
        let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
-
        let path = unsound::path::new("README.md");
+
        let path = Path::new("README.md");
        let mut file_history = history.by_path(path);
        let commit = file_history.next().unwrap().unwrap();
        let file = repo
-
            .get_commit_file(&commit.id, unsound::path::new("README.md"))
+
            .get_commit_file(&commit.id, &Path::new("README.md"))
            .unwrap();
        assert_eq!(file.size(), 67);
    }
modified radicle-surf/t/src/lib.rs
@@ -6,6 +6,3 @@ mod git;

#[cfg(test)]
mod diff;
-

-
#[cfg(test)]
-
mod file_system;