repo_root/
lib.rs

1// TODO: take asref<path> instead of path for arguments
2use color_eyre::eyre::bail;
3use color_eyre::Result;
4use log::debug;
5use projects::{DockerProject, GitProject, NixProject, NodeProject, PythonProject, RustProject};
6use std::env;
7use std::fmt::Display;
8use std::marker::PhantomData;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11use winnow::combinator::alt;
12use winnow::prelude::*;
13use winnow::Parser;
14
15pub mod projects;
16
17/// Traverse a path's ancestry, walking the chain of parents to the root (/) until conditoin is met
18fn traverse_backwards<F>(cwd: &Path, condition: F) -> Result<Option<PathBuf>>
19where
20    F: Fn(&Path) -> bool,
21{
22    let mut cwd = cwd.to_path_buf();
23    loop {
24        if condition(&cwd) {
25            return Ok(Some(cwd.to_path_buf()));
26        }
27
28        let Some(parent) = cwd.parent() else {
29            break;
30        };
31        cwd = parent.to_path_buf();
32    }
33
34    Ok(None)
35}
36
37/// Traverse a path, starting from the root (/) walking down the children to the full path until condition is met
38fn traverse_forward<F>(cwd: &Path, condition: F) -> Result<Option<PathBuf>>
39where
40    F: Fn(&Path) -> bool,
41{
42    let mut path = PathBuf::new();
43    for component in cwd.components() {
44        path = path.join(component);
45        if condition(&path) {
46            return Ok(Some(path));
47        }
48    }
49
50    Ok(None)
51}
52
53/// Do not go up/down the directory tree; only look in cwd
54fn no_traversal<F>(cwd: &Path, condition: F) -> Result<Option<PathBuf>>
55where
56    F: Fn(&Path) -> bool,
57{
58    if condition(&cwd) {
59        return Ok(Some(cwd.to_path_buf()));
60    }
61
62    Ok(None)
63}
64
65// TODO: support looking at env var PRJ_ROOT
66// TODO: golang
67// TODO: zig
68// TODO: Cmake
69
70#[derive(Debug, Copy, Clone)]
71pub enum ProjectTypes {
72    Git,
73    Docker,
74    NodeJS,
75    Rust,
76    Python,
77    Nix,
78}
79
80impl ProjectTypes {
81    pub fn find(&self, path: &Path) -> Result<Option<PathBuf>> {
82        match self {
83            ProjectTypes::Git => {
84                let root: Option<RepoRoot<GitProject>> = RepoRoot::find(path)?;
85                let root = root.map(|r| r.path());
86                Ok(root)
87            }
88
89            ProjectTypes::Docker => {
90                let root: Option<RepoRoot<DockerProject>> = RepoRoot::find(path)?;
91                let root = root.map(|r| r.path());
92                Ok(root)
93            }
94
95            ProjectTypes::NodeJS => {
96                let root: Option<RepoRoot<NodeProject>> = RepoRoot::find(path)?;
97                let root = root.map(|r| r.path());
98                Ok(root)
99            }
100
101            ProjectTypes::Rust => {
102                let root: Option<RepoRoot<RustProject>> = RepoRoot::find(path)?;
103                let root = root.map(|r| r.path());
104                Ok(root)
105            }
106
107            ProjectTypes::Python => {
108                let root: Option<RepoRoot<PythonProject>> = RepoRoot::find(path)?;
109                let root = root.map(|r| r.path());
110                Ok(root)
111            }
112
113            ProjectTypes::Nix => {
114                let root: Option<RepoRoot<NixProject>> = RepoRoot::find(path)?;
115                let root = root.map(|r| r.path());
116                Ok(root)
117            }
118        }
119    }
120}
121
122fn git(s: &mut &str) -> PResult<ProjectTypes> {
123    "git".map(|_| ProjectTypes::Git).parse_next(s)
124}
125
126fn docker(s: &mut &str) -> PResult<ProjectTypes> {
127    "docker".map(|_| ProjectTypes::Docker).parse_next(s)
128}
129
130fn nodejs(s: &mut &str) -> PResult<ProjectTypes> {
131    alt(("node", "js", "nodejs"))
132        .map(|_| ProjectTypes::NodeJS)
133        .parse_next(s)
134}
135
136fn rust(s: &mut &str) -> PResult<ProjectTypes> {
137    "rust".map(|_| ProjectTypes::Rust).parse_next(s)
138}
139
140fn python(s: &mut &str) -> PResult<ProjectTypes> {
141    "python".map(|_| ProjectTypes::Python).parse_next(s)
142}
143
144fn nix(s: &mut &str) -> PResult<ProjectTypes> {
145    "nix".map(|_| ProjectTypes::Nix).parse_next(s)
146}
147
148fn project_type(s: &mut &str) -> PResult<ProjectTypes> {
149    alt((git, docker, nodejs, rust, python, nix)).parse_next(s)
150}
151
152use thiserror::Error;
153#[derive(Debug, Error)]
154pub enum ParseError {
155    #[error("Unable to parse project type")]
156    ProjectType,
157}
158
159impl FromStr for ProjectTypes {
160    type Err = ParseError;
161    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
162        project_type.parse(s).map_err(|_| ParseError::ProjectType)
163    }
164}
165
166impl Display for ProjectTypes {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        let repr = match self {
169            ProjectTypes::Git => "git",
170            ProjectTypes::Docker => "docker",
171            ProjectTypes::NodeJS => "nodejs",
172            ProjectTypes::Rust => "rust",
173            ProjectTypes::Python => "python",
174            ProjectTypes::Nix => "nix",
175        };
176        write!(f, "{}", repr)
177    }
178}
179
180impl Default for ProjectTypes {
181    fn default() -> Self {
182        ProjectTypes::Git
183    }
184}
185
186/// Determine direction to walk between root dir and cwd
187pub enum TraversalDirection {
188    /// Traverse from the root to the cwd
189    Forward,
190    /// Traverse from the cwd to root
191    Backwards,
192    /// Stay in cwd; do not traverse
193    NoTraversal,
194}
195
196pub trait ProjectType {
197    fn direction() -> TraversalDirection;
198    fn condition(path: &Path) -> bool;
199    fn traverse(path: &Path) -> Result<Option<PathBuf>> {
200        match Self::direction() {
201            TraversalDirection::Forward => traverse_forward(path, Self::condition),
202            TraversalDirection::Backwards => traverse_backwards(path, Self::condition),
203            TraversalDirection::NoTraversal => no_traversal(path, Self::condition),
204        }
205    }
206}
207
208pub struct RepoRoot<T: ProjectType> {
209    pub path: PathBuf,
210    pub project_type: PhantomData<T>,
211}
212
213impl<T> RepoRoot<T>
214where
215    T: ProjectType,
216{
217    pub fn new(path: &Path) -> Self {
218        Self {
219            path: path.to_path_buf(),
220            project_type: PhantomData,
221        }
222    }
223
224    /// Extract the path of the RepoRoot
225    pub fn path(&self) -> PathBuf {
226        self.path.clone()
227    }
228
229    pub fn find(path: &Path) -> Result<Option<Self>> {
230        let root = T::traverse(path)?.map(|p| RepoRoot::new(&p));
231        Ok(root)
232    }
233}