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