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.name().ok_or(error::Directory::Utf8Error)?.to_string();
273 let id = entry.id().into();
274
275 match entry.kind() {
276 Some(git2::ObjectType::Tree) => Ok(Self::Directory(Directory::new(name, path, id))),
277 Some(git2::ObjectType::Blob) => Ok(Self::File(File::new(name, path, id))),
278 Some(git2::ObjectType::Commit) => {
279 let submodule = (!repo.is_bare())
280 .then(|| repo.find_submodule(&name))
281 .transpose()?;
282 Ok(Self::Submodule(Submodule::new(name, path, submodule, id)?))
283 }
284 _ => Err(error::Directory::InvalidType(path, "tree or blob")),
285 }
286 }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct Directory {
300 name: String,
302 prefix: PathBuf,
305 id: Oid,
307}
308
309const ROOT_DIR: &str = "";
310
311impl Directory {
312 pub(crate) fn root(id: Oid) -> Self {
316 Self::new(ROOT_DIR.to_string(), PathBuf::new(), id)
317 }
318
319 pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
326 debug_assert!(
327 name.is_empty() || !prefix.ends_with(&name),
328 "prefix = {prefix:?}, name = {name}",
329 );
330 Self { name, prefix, id }
331 }
332
333 pub fn name(&self) -> &String {
335 &self.name
336 }
337
338 pub fn id(&self) -> Oid {
340 self.id
341 }
342
343 pub fn path(&self) -> PathBuf {
348 self.prefix.join(&self.name)
349 }
350
351 pub fn location(&self) -> &Path {
354 &self.prefix
355 }
356
357 pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
368 let tree = repo.find_tree(self.id)?;
369
370 let mut entries = BTreeMap::new();
371 let mut error = None;
372 let path = self.path();
373
374 tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
377 match Entry::from_entry(entry, path.clone(), repo) {
378 Ok(entry) => match entry {
379 Entry::File(_) => {
380 entries.insert(entry.name().clone(), entry);
381 git2::TreeWalkResult::Ok
382 }
383 Entry::Directory(_) => {
384 entries.insert(entry.name().clone(), entry);
385 git2::TreeWalkResult::Skip
387 }
388 Entry::Submodule(_) => {
389 entries.insert(entry.name().clone(), entry);
390 git2::TreeWalkResult::Ok
391 }
392 },
393 Err(err) => {
394 error = Some(err);
395 git2::TreeWalkResult::Abort
396 }
397 }
398 })?;
399
400 match error {
401 Some(err) => Err(err),
402 None => Ok(Entries { listing: entries }),
403 }
404 }
405
406 pub fn find_entry<P>(&self, path: &P, repo: &Repository) -> Result<Entry, error::Directory>
408 where
409 P: AsRef<Path>,
410 {
411 let path = path.as_ref();
413 let git2_tree = repo.find_tree(self.id)?;
414 let entry = git2_tree
415 .get_path(path)
416 .or_matches::<error::Directory, _, _>(is_not_found_err, || {
417 Err(error::Directory::PathNotFound(path.to_path_buf()))
418 })?;
419 let parent = path
420 .parent()
421 .ok_or_else(|| error::Directory::InvalidPath(path.to_path_buf()))?;
422 let root_path = self.path().join(parent);
423
424 Entry::from_entry(&entry, root_path, repo)
425 }
426
427 pub fn find_file<P>(&self, path: &P, repo: &Repository) -> Result<File, error::Directory>
429 where
430 P: AsRef<Path>,
431 {
432 match self.find_entry(path, repo)? {
433 Entry::File(file) => Ok(file),
434 _ => Err(error::Directory::InvalidType(
435 path.as_ref().to_path_buf(),
436 "file",
437 )),
438 }
439 }
440
441 pub fn find_directory<P>(&self, path: &P, repo: &Repository) -> Result<Self, error::Directory>
445 where
446 P: AsRef<Path>,
447 {
448 if path.as_ref() == Path::new(ROOT_DIR) {
449 return Ok(self.clone());
450 }
451
452 match self.find_entry(path, repo)? {
453 Entry::Directory(d) => Ok(d),
454 _ => Err(error::Directory::InvalidType(
455 path.as_ref().to_path_buf(),
456 "directory",
457 )),
458 }
459 }
460
461 #[allow(dead_code)]
464 fn fuzzy_find(_label: &Path) -> Vec<Self> {
465 unimplemented!()
466 }
467
468 pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
471 self.traverse(repo, 0, &mut |size, entry| match entry {
472 Entry::File(file) => Ok(size + file.content(repo)?.size()),
473 Entry::Directory(dir) => Ok(size + dir.size(repo)?),
474 Entry::Submodule(_) => Ok(size),
475 })
476 }
477
478 pub fn traverse<Error, B, F>(
490 &self,
491 repo: &Repository,
492 initial: B,
493 f: &mut F,
494 ) -> Result<B, Error>
495 where
496 Error: From<error::Directory>,
497 F: FnMut(B, &Entry) -> Result<B, Error>,
498 {
499 self.entries(repo)?
500 .entries()
501 .try_fold(initial, |acc, entry| match entry {
502 Entry::File(_) => f(acc, entry),
503 Entry::Directory(directory) => {
504 let acc = directory.traverse(repo, acc, f)?;
505 f(acc, entry)
506 }
507 Entry::Submodule(_) => f(acc, entry),
508 })
509 }
510}
511
512impl Revision for Directory {
513 type Error = Infallible;
514
515 fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
516 Ok(self.id)
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct Submodule {
526 name: String,
527 prefix: PathBuf,
528 id: Oid,
529 url: Option<Url>,
530}
531
532impl Submodule {
533 pub fn new(
541 name: String,
542 prefix: PathBuf,
543 submodule: Option<git2::Submodule>,
544 id: Oid,
545 ) -> Result<Self, error::Submodule> {
546 let url = submodule
547 .and_then(|module| {
548 module
549 .opt_url_bytes()
550 .map(|bs| std::str::from_utf8(bs).map(|url| url.to_string()))
551 })
552 .transpose()
553 .map_err(|err| error::Submodule::Utf8 {
554 name: name.clone(),
555 err,
556 })?;
557 let url = url
558 .map(|url| {
559 Url::parse(&url).map_err(|err| error::Submodule::ParseUrl {
560 name: name.clone(),
561 url,
562 err,
563 })
564 })
565 .transpose()?;
566 Ok(Self {
567 name,
568 prefix,
569 id,
570 url,
571 })
572 }
573
574 pub fn name(&self) -> &String {
576 &self.name
577 }
578
579 pub fn location(&self) -> &Path {
582 &self.prefix
583 }
584
585 pub fn path(&self) -> PathBuf {
590 self.prefix.join(&self.name)
591 }
592
593 pub fn id(&self) -> Oid {
598 self.id
599 }
600
601 pub fn url(&self) -> &Option<Url> {
603 &self.url
604 }
605}