1use std::{
24 cmp::Ordering,
25 collections::BTreeMap,
26 convert::{Infallible, Into as _},
27 path::{Path, PathBuf},
28};
29
30use git2::Blob;
31use radicle_git_ext::{is_not_found_err, Oid};
32use radicle_std_ext::result::ResultExt as _;
33use url::Url;
34
35use crate::{Repository, Revision};
36
37pub mod error {
38 use std::path::PathBuf;
39
40 use thiserror::Error;
41
42 #[derive(Debug, Error, PartialEq)]
43 pub enum Directory {
44 #[error(transparent)]
45 Git(#[from] git2::Error),
46 #[error(transparent)]
47 File(#[from] File),
48 #[error("the path {0} is not valid")]
49 InvalidPath(PathBuf),
50 #[error("the entry at '{0}' must be of type {1}")]
51 InvalidType(PathBuf, &'static str),
52 #[error("the entry name was not valid UTF-8")]
53 Utf8Error,
54 #[error("the path {0} not found")]
55 PathNotFound(PathBuf),
56 #[error(transparent)]
57 Submodule(#[from] Submodule),
58 }
59
60 #[derive(Debug, Error, PartialEq)]
61 pub enum File {
62 #[error(transparent)]
63 Git(#[from] git2::Error),
64 }
65
66 #[derive(Debug, Error, PartialEq)]
67 pub enum Submodule {
68 #[error("URL is invalid utf-8 for submodule '{name}': {err}")]
69 Utf8 {
70 name: String,
71 #[source]
72 err: std::str::Utf8Error,
73 },
74 #[error("failed to parse URL '{url}' for submodule '{name}': {err}")]
75 ParseUrl {
76 name: String,
77 url: String,
78 #[source]
79 err: url::ParseError,
80 },
81 }
82}
83
84#[derive(Clone, PartialEq, Eq, Debug)]
94pub struct File {
95 name: String,
97 prefix: PathBuf,
100 id: Oid,
102}
103
104impl File {
105 pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
112 debug_assert!(
113 !prefix.ends_with(&name),
114 "prefix = {prefix:?}, name = {name}",
115 );
116 Self { name, prefix, id }
117 }
118
119 pub fn name(&self) -> &str {
121 self.name.as_str()
122 }
123
124 pub fn id(&self) -> Oid {
126 self.id
127 }
128
129 pub fn path(&self) -> PathBuf {
134 self.prefix.join(escaped_name(&self.name))
135 }
136
137 pub fn location(&self) -> &Path {
140 &self.prefix
141 }
142
143 pub fn content<'a>(&self, repo: &'a Repository) -> Result<FileContent<'a>, error::File> {
150 let blob = repo.find_blob(self.id)?;
151 Ok(FileContent { blob })
152 }
153}
154
155pub struct FileContent<'a> {
159 blob: Blob<'a>,
160}
161
162impl<'a> FileContent<'a> {
163 pub fn as_bytes(&self) -> &[u8] {
165 self.blob.content()
166 }
167
168 pub fn size(&self) -> usize {
170 self.blob.size()
171 }
172
173 pub(crate) fn new(blob: Blob<'a>) -> Self {
175 Self { blob }
176 }
177}
178
179pub struct Entries {
181 listing: BTreeMap<String, Entry>,
182}
183
184impl Entries {
185 pub fn names(&self) -> impl Iterator<Item = &String> {
187 self.listing.keys()
188 }
189
190 pub fn entries(&self) -> impl Iterator<Item = &Entry> {
192 self.listing.values()
193 }
194
195 pub fn iter(&self) -> impl Iterator<Item = (&String, &Entry)> {
197 self.listing.iter()
198 }
199}
200
201impl Iterator for Entries {
202 type Item = Entry;
203
204 fn next(&mut self) -> Option<Self::Item> {
205 let next_key = match self.listing.keys().next() {
207 Some(k) => k.clone(),
208 None => return None,
209 };
210 self.listing.remove(&next_key)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum Entry {
217 File(File),
219 Directory(Directory),
221 Submodule(Submodule),
223}
224
225impl PartialOrd for Entry {
226 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
227 Some(self.cmp(other))
228 }
229}
230
231impl Ord for Entry {
232 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
233 match (self, other) {
234 (Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
235 (Entry::File(_), Entry::Directory(_)) => Ordering::Less,
236 (Entry::File(_), Entry::Submodule(_)) => Ordering::Less,
237 (Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
238 (Entry::Submodule(_), Entry::File(_)) => Ordering::Less,
239 (Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
240 (Entry::Directory(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
241 (Entry::Submodule(x), Entry::Directory(y)) => x.name().cmp(y.name()),
242 (Entry::Submodule(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
243 }
244 }
245}
246
247impl Entry {
248 pub fn name(&self) -> &String {
251 match self {
252 Entry::File(file) => &file.name,
253 Entry::Directory(directory) => directory.name(),
254 Entry::Submodule(submodule) => submodule.name(),
255 }
256 }
257
258 pub fn path(&self) -> PathBuf {
259 match self {
260 Entry::File(file) => file.path(),
261 Entry::Directory(directory) => directory.path(),
262 Entry::Submodule(submodule) => submodule.path(),
263 }
264 }
265
266 pub fn location(&self) -> &Path {
267 match self {
268 Entry::File(file) => file.location(),
269 Entry::Directory(directory) => directory.location(),
270 Entry::Submodule(submodule) => submodule.location(),
271 }
272 }
273
274 pub fn is_file(&self) -> bool {
276 matches!(self, Entry::File(_))
277 }
278
279 pub fn is_directory(&self) -> bool {
281 matches!(self, Entry::Directory(_))
282 }
283
284 pub(crate) fn from_entry(
285 entry: &git2::TreeEntry,
286 path: PathBuf,
287 repo: &Repository,
288 ) -> Result<Self, error::Directory> {
289 let name = entry.name().ok_or(error::Directory::Utf8Error)?.to_string();
290 let id = entry.id().into();
291
292 match entry.kind() {
293 Some(git2::ObjectType::Tree) => Ok(Self::Directory(Directory::new(name, path, id))),
294 Some(git2::ObjectType::Blob) => Ok(Self::File(File::new(name, path, id))),
295 Some(git2::ObjectType::Commit) => {
296 let submodule = (!repo.is_bare())
297 .then(|| repo.find_submodule(&name))
298 .transpose()?;
299 Ok(Self::Submodule(Submodule::new(name, path, submodule, id)?))
300 }
301 _ => Err(error::Directory::InvalidType(path, "tree or blob")),
302 }
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct Directory {
317 name: String,
319 prefix: PathBuf,
322 id: Oid,
324}
325
326const ROOT_DIR: &str = "";
327
328impl Directory {
329 pub(crate) fn root(id: Oid) -> Self {
333 Self::new(ROOT_DIR.to_string(), PathBuf::new(), id)
334 }
335
336 pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
343 debug_assert!(
344 name.is_empty() || !prefix.ends_with(&name),
345 "prefix = {prefix:?}, name = {name}",
346 );
347 Self { name, prefix, id }
348 }
349
350 pub fn name(&self) -> &String {
352 &self.name
353 }
354
355 pub fn id(&self) -> Oid {
357 self.id
358 }
359
360 pub fn path(&self) -> PathBuf {
365 self.prefix.join(escaped_name(&self.name))
366 }
367
368 pub fn location(&self) -> &Path {
371 &self.prefix
372 }
373
374 pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
385 let tree = repo.find_tree(self.id)?;
386
387 let mut entries = BTreeMap::new();
388 let mut error = None;
389 let path = self.path();
390
391 tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
394 match Entry::from_entry(entry, path.clone(), repo) {
395 Ok(entry) => match entry {
396 Entry::File(_) => {
397 entries.insert(entry.name().clone(), entry);
398 git2::TreeWalkResult::Ok
399 }
400 Entry::Directory(_) => {
401 entries.insert(entry.name().clone(), entry);
402 git2::TreeWalkResult::Skip
404 }
405 Entry::Submodule(_) => {
406 entries.insert(entry.name().clone(), entry);
407 git2::TreeWalkResult::Ok
408 }
409 },
410 Err(err) => {
411 error = Some(err);
412 git2::TreeWalkResult::Abort
413 }
414 }
415 })?;
416
417 match error {
418 Some(err) => Err(err),
419 None => Ok(Entries { listing: entries }),
420 }
421 }
422
423 pub fn find_entry<P>(&self, path: &P, repo: &Repository) -> Result<Entry, error::Directory>
425 where
426 P: AsRef<Path>,
427 {
428 let path = path.as_ref();
430 let git2_tree = repo.find_tree(self.id)?;
431 let entry = git2_tree
432 .get_path(path)
433 .or_matches::<error::Directory, _, _>(is_not_found_err, || {
434 Err(error::Directory::PathNotFound(path.to_path_buf()))
435 })?;
436 let parent = path
437 .parent()
438 .ok_or_else(|| error::Directory::InvalidPath(path.to_path_buf()))?;
439 let root_path = self.path().join(parent);
440
441 Entry::from_entry(&entry, root_path, repo)
442 }
443
444 pub fn find_file<P>(&self, path: &P, repo: &Repository) -> Result<File, error::Directory>
446 where
447 P: AsRef<Path>,
448 {
449 match self.find_entry(path, repo)? {
450 Entry::File(file) => Ok(file),
451 _ => Err(error::Directory::InvalidType(
452 path.as_ref().to_path_buf(),
453 "file",
454 )),
455 }
456 }
457
458 pub fn find_directory<P>(&self, path: &P, repo: &Repository) -> Result<Self, error::Directory>
462 where
463 P: AsRef<Path>,
464 {
465 if path.as_ref() == Path::new(ROOT_DIR) {
466 return Ok(self.clone());
467 }
468
469 match self.find_entry(path, repo)? {
470 Entry::Directory(d) => Ok(d),
471 _ => Err(error::Directory::InvalidType(
472 path.as_ref().to_path_buf(),
473 "directory",
474 )),
475 }
476 }
477
478 #[allow(dead_code)]
481 fn fuzzy_find(_label: &Path) -> Vec<Self> {
482 unimplemented!()
483 }
484
485 pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
488 self.traverse(repo, 0, &mut |size, entry| match entry {
489 Entry::File(file) => Ok(size + file.content(repo)?.size()),
490 Entry::Directory(dir) => Ok(size + dir.size(repo)?),
491 Entry::Submodule(_) => Ok(size),
492 })
493 }
494
495 pub fn traverse<Error, B, F>(
507 &self,
508 repo: &Repository,
509 initial: B,
510 f: &mut F,
511 ) -> Result<B, Error>
512 where
513 Error: From<error::Directory>,
514 F: FnMut(B, &Entry) -> Result<B, Error>,
515 {
516 self.entries(repo)?
517 .entries()
518 .try_fold(initial, |acc, entry| match entry {
519 Entry::File(_) => f(acc, entry),
520 Entry::Directory(directory) => {
521 let acc = directory.traverse(repo, acc, f)?;
522 f(acc, entry)
523 }
524 Entry::Submodule(_) => f(acc, entry),
525 })
526 }
527}
528
529impl Revision for Directory {
530 type Error = Infallible;
531
532 fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
533 Ok(self.id)
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct Submodule {
543 name: String,
544 prefix: PathBuf,
545 id: Oid,
546 url: Option<Url>,
547}
548
549impl Submodule {
550 pub fn new(
558 name: String,
559 prefix: PathBuf,
560 submodule: Option<git2::Submodule>,
561 id: Oid,
562 ) -> Result<Self, error::Submodule> {
563 let url = submodule
564 .and_then(|module| {
565 module
566 .opt_url_bytes()
567 .map(|bs| std::str::from_utf8(bs).map(|url| url.to_string()))
568 })
569 .transpose()
570 .map_err(|err| error::Submodule::Utf8 {
571 name: name.clone(),
572 err,
573 })?;
574 let url = url
575 .map(|url| {
576 Url::parse(&url).map_err(|err| error::Submodule::ParseUrl {
577 name: name.clone(),
578 url,
579 err,
580 })
581 })
582 .transpose()?;
583 Ok(Self {
584 name,
585 prefix,
586 id,
587 url,
588 })
589 }
590
591 pub fn name(&self) -> &String {
593 &self.name
594 }
595
596 pub fn location(&self) -> &Path {
599 &self.prefix
600 }
601
602 pub fn path(&self) -> PathBuf {
607 self.prefix.join(escaped_name(&self.name))
608 }
609
610 pub fn id(&self) -> Oid {
615 self.id
616 }
617
618 pub fn url(&self) -> &Option<Url> {
620 &self.url
621 }
622}
623
624fn escaped_name(name: &str) -> String {
627 name.replace('\\', r"\\")
628}