Krolyxon
Commit 4e844bb

ferreniumsrcgittree.rs

147 lines · 4.3 KB · Rust

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