1use std::path::{Path, PathBuf};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::config::Config;
7use crate::detect::{BuildSystem, VcsType};
8use crate::error::PrjError;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Project {
13 pub name: String,
14 pub path: PathBuf,
15 pub vcs: Vec<VcsType>,
16 pub build_systems: Vec<BuildSystem>,
17 pub artifact_dirs: Vec<String>,
18 pub added_at: DateTime<Utc>,
19 #[serde(default)]
20 pub tags: Vec<String>,
21}
22
23#[derive(Debug, Default, Serialize, Deserialize)]
25pub struct ProjectDatabase {
26 #[serde(default)]
27 pub projects: Vec<Project>,
28}
29
30impl ProjectDatabase {
31 pub fn load(config: &Config) -> Result<Self, PrjError> {
33 let path = config.database_path();
34 if path.exists() {
35 let content =
36 std::fs::read_to_string(&path).map_err(|e| PrjError::DatabaseRead(Box::new(e)))?;
37 toml::from_str(&content).map_err(|e| PrjError::DatabaseRead(Box::new(e)))
38 } else {
39 Ok(Self::default())
40 }
41 }
42
43 pub fn save(&self, config: &Config) -> Result<(), PrjError> {
45 let path = config.database_path();
46 if let Some(parent) = path.parent() {
47 std::fs::create_dir_all(parent).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
48 }
49 let content =
50 toml::to_string_pretty(self).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
51 std::fs::write(&path, content).map_err(|e| PrjError::DatabaseWrite(Box::new(e)))?;
52 Ok(())
53 }
54
55 pub fn add(&mut self, project: Project) -> Result<(), PrjError> {
57 if self.projects.iter().any(|p| p.path == project.path) {
58 return Err(PrjError::ProjectAlreadyExists(
59 project.path.display().to_string(),
60 ));
61 }
62 self.projects.push(project);
63 Ok(())
64 }
65
66 pub fn remove(&mut self, name: &str) -> Result<Project, PrjError> {
68 let idx = self
69 .projects
70 .iter()
71 .position(|p| p.name == name)
72 .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
73 Ok(self.projects.remove(idx))
74 }
75
76 pub fn find(&self, name: &str) -> Option<&Project> {
78 self.projects.iter().find(|p| p.name == name)
79 }
80
81 pub fn find_mut(&mut self, name: &str) -> Option<&mut Project> {
83 self.projects.iter_mut().find(|p| p.name == name)
84 }
85
86 pub fn add_tags(&mut self, name: &str, tags: &[String]) -> Result<(), PrjError> {
88 let project = self
89 .find_mut(name)
90 .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
91 for tag in tags {
92 if !project.tags.contains(tag) {
93 project.tags.push(tag.clone());
94 }
95 }
96 project.tags.sort();
97 Ok(())
98 }
99
100 pub fn remove_tags(&mut self, name: &str, tags: &[String]) -> Result<(), PrjError> {
102 let project = self
103 .find_mut(name)
104 .ok_or_else(|| PrjError::ProjectNotFound(name.to_string()))?;
105 project.tags.retain(|t| !tags.contains(t));
106 Ok(())
107 }
108
109 pub fn find_orphaned(&self) -> Vec<&Project> {
111 self.projects.iter().filter(|p| !p.path.exists()).collect()
112 }
113
114 pub fn remove_orphaned(&mut self) -> Vec<Project> {
116 let (orphaned, alive): (Vec<_>, Vec<_>) = std::mem::take(&mut self.projects)
117 .into_iter()
118 .partition(|p| !p.path.exists());
119 self.projects = alive;
120 orphaned
121 }
122
123 pub fn register(&mut self, path: &Path, name: Option<&str>) -> Result<&Project, PrjError> {
125 let path = path
126 .canonicalize()
127 .map_err(|_| PrjError::PathNotFound(path.to_path_buf()))?;
128
129 if !path.is_dir() {
130 return Err(PrjError::NotADirectory(path));
131 }
132
133 let detection = crate::detect::detect_project(&path);
134
135 let name = name.map(|s| s.to_string()).unwrap_or_else(|| {
136 path.file_name()
137 .map(|n| n.to_string_lossy().to_string())
138 .unwrap_or_else(|| "unknown".to_string())
139 });
140
141 let project = Project {
142 name,
143 path,
144 vcs: detection.vcs,
145 build_systems: detection.build_systems,
146 artifact_dirs: detection.artifact_dirs,
147 added_at: Utc::now(),
148 tags: Vec::new(),
149 };
150
151 self.add(project)?;
152 Ok(self.projects.last().expect("just pushed"))
153 }
154}