Krolyxon
Commit 55c1225

ferreniumsrcgittree.rs

125 lines · 3.4 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 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)]
42pub 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)]
52pub enum PathTarget {
53 Tree(ObjectId),
54 Blob(ObjectId, EntryKind),
55 Submodule(ObjectId),
56}
57
58fn 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.
63pub 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.
88pub 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.
117pub 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.
123pub fn size(repo: &gix::Repository, oid: ObjectId) -> anyhow::Result<u64> {
124 Ok(repo.find_header(oid)?.size())
125}