vibe_graph_ops/
project.rs1use std::path::PathBuf;
4
5use file_format::FileFormat;
6use humansize::{format_size, DECIMAL};
7use serde::{Deserialize, Serialize};
8
9use crate::error::OpsResult;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Project {
14 pub name: String,
16
17 pub source: ProjectSource,
19
20 pub repositories: Vec<Repository>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum ProjectSource {
30 GitHubOrg { organization: String },
32 GitHubRepo { owner: String, repo: String },
34 LocalPath { path: PathBuf },
36 LocalPaths { paths: Vec<PathBuf> },
38}
39
40impl Project {
41 pub fn github_org(organization: impl Into<String>) -> Self {
43 let org = organization.into();
44 Self {
45 name: org.clone(),
46 source: ProjectSource::GitHubOrg { organization: org },
47 repositories: vec![],
48 }
49 }
50
51 pub fn github_repo(owner: impl Into<String>, repo: impl Into<String>) -> Self {
53 let owner = owner.into();
54 let repo = repo.into();
55 Self {
56 name: repo.clone(),
57 source: ProjectSource::GitHubRepo { owner, repo },
58 repositories: vec![],
59 }
60 }
61
62 pub fn local(path: PathBuf) -> Self {
64 let name = path
65 .file_name()
66 .map(|s| s.to_string_lossy().to_string())
67 .unwrap_or_else(|| "local".to_string());
68 Self {
69 name,
70 source: ProjectSource::LocalPath { path },
71 repositories: vec![],
72 }
73 }
74
75 pub fn local_paths(name: impl Into<String>, paths: Vec<PathBuf>) -> Self {
77 Self {
78 name: name.into(),
79 source: ProjectSource::LocalPaths { paths },
80 repositories: vec![],
81 }
82 }
83
84 pub fn expand_content<F>(&mut self, filter_fn: F) -> OpsResult<()>
86 where
87 F: Fn(&Source) -> bool,
88 {
89 for repo in &mut self.repositories {
90 for source in &mut repo.sources {
91 if filter_fn(source) {
92 source.content = std::fs::read_to_string(&source.path).ok();
93 }
94 }
95 }
96 Ok(())
97 }
98
99 pub fn total_sources(&self) -> usize {
101 self.repositories.iter().map(|r| r.sources.len()).sum()
102 }
103
104 pub fn total_size(&self) -> u64 {
106 self.repositories
107 .iter()
108 .flat_map(|r| &r.sources)
109 .filter_map(|s| s.size)
110 .sum()
111 }
112
113 pub fn human_total_size(&self) -> String {
115 format_size(self.total_size(), DECIMAL)
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Repository {
122 pub name: String,
124
125 pub url: String,
127
128 pub local_path: PathBuf,
130
131 pub sources: Vec<Source>,
133}
134
135impl Repository {
136 pub fn new(name: impl Into<String>, url: impl Into<String>, local_path: PathBuf) -> Self {
138 Self {
139 name: name.into(),
140 url: url.into(),
141 local_path,
142 sources: vec![],
143 }
144 }
145
146 pub fn total_size(&self) -> u64 {
148 self.sources.iter().filter_map(|s| s.size).sum()
149 }
150
151 pub fn human_total_size(&self) -> String {
153 format_size(self.total_size(), DECIMAL)
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct Source {
160 pub path: PathBuf,
162
163 pub relative_path: String,
165
166 #[serde(with = "file_format_serde")]
168 pub format: FileFormat,
169
170 pub size: Option<u64>,
172
173 pub content: Option<String>,
175}
176
177impl Source {
178 pub fn from_path(path: PathBuf, repo_root: &PathBuf) -> OpsResult<Self> {
180 let relative_path = path
181 .strip_prefix(repo_root)
182 .map(|p| p.to_string_lossy().to_string())
183 .unwrap_or_else(|_| path.to_string_lossy().to_string());
184
185 let format = FileFormat::from_file(&path).unwrap_or(FileFormat::ArbitraryBinaryData);
186 let size = std::fs::metadata(&path).ok().map(|m| m.len());
187
188 Ok(Self {
189 path,
190 relative_path,
191 format,
192 size,
193 content: None,
194 })
195 }
196
197 pub fn human_size(&self) -> String {
199 self.size
200 .map(|s| format_size(s, DECIMAL))
201 .unwrap_or_else(|| "unknown".to_string())
202 }
203
204 pub fn is_text(&self) -> bool {
206 matches!(
207 self.format,
208 FileFormat::ArbitraryBinaryData | FileFormat::PlainText
209 )
210 }
211
212 pub fn extension(&self) -> Option<&str> {
214 self.path.extension().and_then(|e| e.to_str())
215 }
216}
217
218mod file_format_serde {
220 use file_format::FileFormat;
221 use serde::{Deserialize, Deserializer, Serialize, Serializer};
222
223 pub fn serialize<S>(format: &FileFormat, serializer: S) -> Result<S::Ok, S::Error>
224 where
225 S: Serializer,
226 {
227 format.to_string().serialize(serializer)
228 }
229
230 pub fn deserialize<'de, D>(deserializer: D) -> Result<FileFormat, D::Error>
231 where
232 D: Deserializer<'de>,
233 {
234 let s = String::deserialize(deserializer)?;
235 Ok(if s == "Plain Text" {
236 FileFormat::PlainText
237 } else {
238 FileFormat::ArbitraryBinaryData
239 })
240 }
241}