| 1 | //! Trees and blobs at a commit. |
| 2 | |
| 3 | use std::ffi::OsStr; |
| 4 | use std::os::unix::ffi::OsStrExt; |
| 5 | use std::path::Path; |
| 6 | |
| 7 | use anyhow::Context; |
| 8 | use gix::bstr::{BString, ByteSlice}; |
| 9 | use gix::object::tree::EntryKind as GixKind; |
| 10 | |
| 11 | use super::ObjectId; |
| 12 | |
| 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 14 | pub enum EntryKind { |
| 15 | Tree, |
| 16 | Blob, |
| 17 | Executable, |
| 18 | Symlink, |
| 19 | Submodule, |
| 20 | } |
| 21 | |
| 22 | impl EntryKind { |
| 23 | fn from_gix(kind: GixKind) -> Self { |
| 24 | match kind { |
| 25 | GixKind::Tree => EntryKind::Tree, |
| 26 | GixKind::Blob => EntryKind::Blob, |
| 27 | GixKind::BlobExecutable => EntryKind::Executable, |
| 28 | GixKind::Link => EntryKind::Symlink, |
| 29 | GixKind::Commit => EntryKind::Submodule, |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | pub fn is_blob(self) -> bool { |
| 34 | matches!( |
| 35 | self, |
| 36 | EntryKind::Blob | EntryKind::Executable | EntryKind::Symlink |
| 37 | ) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | #[derive(Debug, Clone)] |
| 42 | pub struct TreeEntry { |
| 43 | pub name: BString, |
| 44 | pub kind: EntryKind, |
| 45 | pub oid: ObjectId, |
| 46 | /// Blob size in bytes (blobs and symlinks only). |
| 47 | pub size: Option<u64>, |
| 48 | } |
| 49 | |
| 50 | /// What a path inside a commit's tree points to. |
| 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 52 | pub enum PathTarget { |
| 53 | Tree(ObjectId), |
| 54 | Blob(ObjectId, EntryKind), |
| 55 | Submodule(ObjectId), |
| 56 | } |
| 57 | |
| 58 | fn as_path(bytes: &[u8]) -> &Path { |
| 59 | Path::new(OsStr::from_bytes(bytes)) |
| 60 | } |
| 61 | |
| 62 | /// Look up `path` (slash-separated, empty for the root) in `commit`'s tree. |
| 63 | pub fn lookup( |
| 64 | repo: &gix::Repository, |
| 65 | commit: ObjectId, |
| 66 | path: &[u8], |
| 67 | ) -> anyhow::Result<Option<PathTarget>> { |
| 68 | let tree = repo |
| 69 | .find_commit(commit) |
| 70 | .context("commit not found")? |
| 71 | .tree() |
| 72 | .context("commit has no tree")?; |
| 73 | if path.is_empty() { |
| 74 | return Ok(Some(PathTarget::Tree(tree.id))); |
| 75 | } |
| 76 | let Some(entry) = tree.lookup_entry_by_path(as_path(path))? else { |
| 77 | return Ok(None); |
| 78 | }; |
| 79 | let oid = entry.object_id(); |
| 80 | Ok(Some(match EntryKind::from_gix(entry.mode().kind()) { |
| 81 | EntryKind::Tree => PathTarget::Tree(oid), |
| 82 | EntryKind::Submodule => PathTarget::Submodule(oid), |
| 83 | kind => PathTarget::Blob(oid, kind), |
| 84 | })) |
| 85 | } |
| 86 | |
| 87 | /// Entries of a tree: directories first, then files, each by name. |
| 88 | pub fn list(repo: &gix::Repository, tree: ObjectId) -> anyhow::Result<Vec<TreeEntry>> { |
| 89 | let tree = repo.find_tree(tree).context("tree not found")?; |
| 90 | let mut entries = Vec::new(); |
| 91 | for entry in tree.iter() { |
| 92 | let entry = entry?; |
| 93 | let kind = EntryKind::from_gix(entry.mode().kind()); |
| 94 | let oid = entry.oid().to_owned(); |
| 95 | let size = kind |
| 96 | .is_blob() |
| 97 | .then(|| repo.find_header(oid).ok().map(|h| h.size())) |
| 98 | .flatten(); |
| 99 | entries.push(TreeEntry { |
| 100 | name: entry.filename().to_owned(), |
| 101 | kind, |
| 102 | oid, |
| 103 | size, |
| 104 | }); |
| 105 | } |
| 106 | entries.sort_by(|a, b| { |
| 107 | let dir = |e: &TreeEntry| e.kind != EntryKind::Tree; |
| 108 | dir(a) |
| 109 | .cmp(&dir(b)) |
| 110 | .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())) |
| 111 | .then_with(|| a.name.cmp(&b.name)) |
| 112 | }); |
| 113 | Ok(entries) |
| 114 | } |
| 115 | |
| 116 | /// Full contents of a blob. |
| 117 | pub fn blob(repo: &gix::Repository, oid: ObjectId) -> anyhow::Result<Vec<u8>> { |
| 118 | let mut blob = repo.find_blob(oid).context("blob not found")?; |
| 119 | Ok(blob.take_data()) |
| 120 | } |
| 121 | |
| 122 | /// Size of an object without reading its contents. |
| 123 | pub fn size(repo: &gix::Repository, oid: ObjectId) -> anyhow::Result<u64> { |
| 124 | Ok(repo.find_header(oid)?.size()) |
| 125 | } |