1use 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
17fn 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
37fn 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
53fn 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#[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
186pub enum TraversalDirection {
188 Forward,
190 Backwards,
192 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 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}