1use std::{
7 cmp::Ordering,
8 collections::BTreeMap,
9 convert::{Infallible, Into as _},
10 path::{Path, PathBuf},
11};
12
13use git2::Blob;
14use radicle_git_ext::{is_not_found_err, Oid};
15use radicle_std_ext::result::ResultExt as _;
16use url::Url;
17
18use crate::{Repository, Revision};
19
20pub mod error {
21 use std::path::PathBuf;
22
23 use thiserror::Error;
24
25 #[derive(Debug, Error, PartialEq)]
26 pub enum Directory {
27 #[error(transparent)]
28 Git(#[from] git2::Error),
29 #[error(transparent)]
30 File(#[from] File),
31 #[error("the path {0} is not valid")]
32 InvalidPath(PathBuf),
33 #[error("the entry at '{0}' must be of type {1}")]
34 InvalidType(PathBuf, &'static str),
35 #[error("the entry name was not valid UTF-8")]
36 Utf8Error,
37 #[error("the path {0} not found")]
38 PathNotFound(PathBuf),
39 #[error(transparent)]
40 Submodule(#[from] Submodule),
41 }
42
43 #[derive(Debug, Error, PartialEq)]
44 pub enum File {
45 #[error(transparent)]
46 Git(#[from] git2::Error),
47 }
48
49 #[derive(Debug, Error, PartialEq)]
50 pub enum Submodule {
51 #[error("URL is invalid utf-8 for submodule '{name}': {err}")]
52 Utf8 {
53 name: String,
54 #[source]
55 err: std::str::Utf8Error,
56 },
57 #[error("failed to parse URL '{url}' for submodule '{name}': {err}")]
58 ParseUrl {
59 name: String,
60 url: String,
61 #[source]
62 err: url::ParseError,
63 },
64 }
65}
66
67#[derive(Clone, PartialEq, Eq, Debug)]
77pub struct File {
78 name: String,
80 prefix: PathBuf,
83 id: Oid,
85}
86
87impl File {
88 pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
95 debug_assert!(
96 !prefix.ends_with(&name),
97 "prefix = {prefix:?}, name = {name}",
98 );
99 Self { name, prefix, id }
100 }
101
102 pub fn name(&self) -> &str {
104 self.name.as_str()
105 }
106
107 pub fn id(&self) -> Oid {
109 self.id
110 }
111
112 pub fn path(&self) -> PathBuf {
117 self.prefix.join(&self.name)
118 }
119
120 pub fn location(&self) -> &Path {
123 &self.prefix
124 }
125
126 pub fn content<'a>(&self, repo: &'a Repository) -> Result<FileContent<'a>, error::File> {
133 let blob = repo.find_blob(self.id)?;
134 Ok(FileContent { blob })
135 }
136}
137
138pub struct FileContent<'a> {
142 blob: Blob<'a>,
143}
144
145impl<'a> FileContent<'a> {
146 pub fn as_bytes(&self) -> &[u8] {
148 self.blob.content()
149 }
150
151 pub fn size(&self) -> usize {
153 self.blob.size()
154 }
155
156 pub(crate) fn new(blob: Blob<'a>) -> Self {
158 Self { blob }
159 }
160}
161
162pub struct Entries {
164 listing: BTreeMap<String, Entry>,
165}
166
167impl Entries {
168 pub fn names(&self) -> impl Iterator<Item = &String> {
170 self.listing.keys()
171 }
172
173 pub fn entries(&self) -> impl Iterator<Item = &Entry> {
175 self.listing.values()
176 }
177
178 pub fn iter(&self) -> impl Iterator<Item = (&String, &Entry)> {
180 self.listing.iter()
181 }
182}
183
184impl Iterator for Entries {
185 type Item = Entry;
186
187 fn next(&mut self) -> Option<Self::Item> {
188 let next_key = match self.listing.keys().next() {
190 Some(k) => k.clone(),
191 None => return None,
192 };
193 self.listing.remove(&next_key)
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum Entry {
200 File(File),
202 Directory(Directory),
204 Submodule(Submodule),
206}
207
208impl PartialOrd for Entry {
209 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
210 Some(self.cmp(other))
211 }
212}
213
214impl Ord for Entry {
215 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
216 match (self, other) {
217 (Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
218 (Entry::File(_), Entry::Directory(_)) => Ordering::Less,
219 (Entry::File(_), Entry::Submodule(_)) => Ordering::Less,
220 (Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
221 (Entry::Submodule(_), Entry::File(_)) => Ordering::Less,
222 (Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
223 (Entry::Directory(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
224 (Entry::Submodule(x), Entry::Directory(y)) => x.name().cmp(y.name()),
225 (Entry::Submodule(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
226 }
227 }
228}
229
230impl Entry {
231 pub fn name(&self) -> &String {
234 match self {
235 Entry::File(file) => &file.name,
236 Entry::Directory(directory) => directory.name(),
237 Entry::Submodule(submodule) => submodule.name(),
238 }
239 }
240
241 pub fn path(&self) -> PathBuf {
242 match self {
243 Entry::File(file) => file.path(),
244 Entry::Directory(directory) => directory.path(),
245 Entry::Submodule(submodule) => submodule.path(),
246 }
247 }
248
249 pub fn location(&self) -> &Path {
250 match self {
251 Entry::File(file) => file.location(),
252 Entry::Directory(directory) => directory.location(),
253 Entry::Submodule(submodule) => submodule.location(),
254 }
255 }
256
257 pub fn is_file(&self) -> bool {
259 matches!(self, Entry::File(_))
260 }
261
262 pub fn is_directory(&self) -> bool {
264 matches!(self, Entry::Directory(_))
265 }
266
267 pub(crate) fn from_entry(
268 entry: &git2::TreeEntry,
269 path: PathBuf,
270 repo: &Repository,
271 ) -> Result<Self, error::Directory> {
272 let name = entry
273 .name()
274 .map_err(|_| error::Directory::Utf8Error)?
275 .to_string();
276 let id = entry.id().into();
277
278 match entry.kind() {
279 Some(git2::ObjectType::Tree) => Ok(Self::Directory(Directory::new(name, path, id))),
280 Some(git2::ObjectType::Blob) => Ok(Self::File(File::new(name, path, id))),
281 Some(git2::ObjectType::Commit) => {
282 let submodule = (!repo.is_bare())
283 .then(|| repo.find_submodule(&name))
284 .transpose()?;
285 Ok(Self::Submodule(Submodule::new(name, path, submodule, id)?))
286 }
287 _ => Err(error::Directory::InvalidType(path, "tree or blob")),
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct Directory {
303 name: String,
305 prefix: PathBuf,
308 id: Oid,
310}
311
312const ROOT_DIR: &str = "";
313
314impl Directory {
315 pub(crate) fn root(id: Oid) -> Self {
319 Self::new(ROOT_DIR.to_string(), PathBuf::new(), id)
320 }
321
322 pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
329 debug_assert!(
330 name.is_empty() || !prefix.ends_with(&name),
331 "prefix = {prefix:?}, name = {name}",
332 );
333 Self { name, prefix, id }
334 }
335
336 pub fn name(&self) -> &String {
338 &self.name
339 }
340
341 pub fn id(&self) -> Oid {
343 self.id
344 }
345
346 pub fn path(&self) -> PathBuf {
351 self.prefix.join(&self.name)
352 }
353
354 pub fn location(&self) -> &Path {
357 &self.prefix
358 }
359
360 pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
371 let tree = repo.find_tree(self.id)?;
372
373 let mut entries = BTreeMap::new();
374 let mut error = None;
375 let path = self.path();
376
377 tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
380 match Entry::from_entry(entry, path.clone(), repo) {
381 Ok(entry) => match entry {
382 Entry::File(_) => {
383 entries.insert(entry.name().clone(), entry);
384 git2::TreeWalkResult::Ok
385 }
386 Entry::Directory(_) => {
387 entries.insert(entry.name().clone(), entry);
388 git2::TreeWalkResult::Skip
390 }
391 Entry::Submodule(_) => {
392 entries.insert(entry.name().clone(), entry);
393 git2::TreeWalkResult::Ok
394 }
395 },
396 Err(err) => {
397 error = Some(err);
398 git2::TreeWalkResult::Abort
399 }
400 }
401 })?;
402
403 match error {
404 Some(err) => Err(err),
405 None => Ok(Entries { listing: entries }),
406 }
407 }
408
409 pub fn find_entry<P>(&self, path: &P, repo: &Repository) -> Result<Entry, error::Directory>
411 where
412 P: AsRef<Path>,
413 {
414 let path = path.as_ref();
416 let git2_tree = repo.find_tree(self.id)?;
417 let entry = git2_tree
418 .get_path(path)
419 .or_matches::<error::Directory, _, _>(is_not_found_err, || {
420 Err(error::Directory::PathNotFound(path.to_path_buf()))
421 })?;
422 let parent = path
423 .parent()
424 .ok_or_else(|| error::Directory::InvalidPath(path.to_path_buf()))?;
425 let root_path = self.path().join(parent);
426
427 Entry::from_entry(&entry, root_path, repo)
428 }
429
430 pub fn find_file<P>(&self, path: &P, repo: &Repository) -> Result<File, error::Directory>
432 where
433 P: AsRef<Path>,
434 {
435 match self.find_entry(path, repo)? {
436 Entry::File(file) => Ok(file),
437 _ => Err(error::Directory::InvalidType(
438 path.as_ref().to_path_buf(),
439 "file",
440 )),
441 }
442 }
443
444 pub fn find_directory<P>(&self, path: &P, repo: &Repository) -> Result<Self, error::Directory>
448 where
449 P: AsRef<Path>,
450 {
451 if path.as_ref() == Path::new(ROOT_DIR) {
452 return Ok(self.clone());
453 }
454
455 match self.find_entry(path, repo)? {
456 Entry::Directory(d) => Ok(d),
457 _ => Err(error::Directory::InvalidType(
458 path.as_ref().to_path_buf(),
459 "directory",
460 )),
461 }
462 }
463
464 #[allow(dead_code)]
467 fn fuzzy_find(_label: &Path) -> Vec<Self> {
468 unimplemented!()
469 }
470
471 pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
474 self.traverse(repo, 0, &mut |size, entry| match entry {
475 Entry::File(file) => Ok(size + file.content(repo)?.size()),
476 Entry::Directory(dir) => Ok(size + dir.size(repo)?),
477 Entry::Submodule(_) => Ok(size),
478 })
479 }
480
481 pub fn traverse<Error, B, F>(
493 &self,
494 repo: &Repository,
495 initial: B,
496 f: &mut F,
497 ) -> Result<B, Error>
498 where
499 Error: From<error::Directory>,
500 F: FnMut(B, &Entry) -> Result<B, Error>,
501 {
502 self.entries(repo)?
503 .entries()
504 .try_fold(initial, |acc, entry| match entry {
505 Entry::File(_) => f(acc, entry),
506 Entry::Directory(directory) => {
507 let acc = directory.traverse(repo, acc, f)?;
508 f(acc, entry)
509 }
510 Entry::Submodule(_) => f(acc, entry),
511 })
512 }
513}
514
515impl Revision for Directory {
516 type Error = Infallible;
517
518 fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
519 Ok(self.id)
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq)]
528pub struct Submodule {
529 name: String,
530 prefix: PathBuf,
531 id: Oid,
532 url: Option<Url>,
533}
534
535impl Submodule {
536 pub fn new(
544 name: String,
545 prefix: PathBuf,
546 submodule: Option<git2::Submodule>,
547 id: Oid,
548 ) -> Result<Self, error::Submodule> {
549 let url = submodule
550 .and_then(|module| {
551 module
552 .opt_url_bytes()
553 .map(|bs| std::str::from_utf8(bs).map(|url| url.to_string()))
554 })
555 .transpose()
556 .map_err(|err| error::Submodule::Utf8 {
557 name: name.clone(),
558 err,
559 })?;
560 let url = url
561 .map(|url| {
562 Url::parse(&url).map_err(|err| error::Submodule::ParseUrl {
563 name: name.clone(),
564 url,
565 err,
566 })
567 })
568 .transpose()?;
569 Ok(Self {
570 name,
571 prefix,
572 id,
573 url,
574 })
575 }
576
577 pub fn name(&self) -> &String {
579 &self.name
580 }
581
582 pub fn location(&self) -> &Path {
585 &self.prefix
586 }
587
588 pub fn path(&self) -> PathBuf {
593 self.prefix.join(&self.name)
594 }
595
596 pub fn id(&self) -> Oid {
601 self.id
602 }
603
604 pub fn url(&self) -> &Option<Url> {
606 &self.url
607 }
608}