feat: add `git ents toolchain view` with on-disk footprint
commit
60f5353feat: add `git ents toolchain view` with on-disk footprint
A gh-CLI-style single-entity view, separate from toolchain list: shows
a toolchain’s recipe/version/platform provenance alongside its disk
usage, summed from the git trees backing bin/src rather than
requiring a local checkout.
feat: add git_toolchain::disk_usage, recursively summing blob sizes
feat: add ToolchainAction::View and toolchain_view CLI command
Assisted-by: Claude:claude-sonnet-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents/src/main.rs
@@ -298,6 +298,13 @@
#[facet(args::positional)]
name: String,
},
+ /// Show a remote's toolchain `name`: its recipe/version/platform
+ /// provenance and its on-disk footprint (`bin`/`src` byte sizes).
+ View {
+ /// Name (`toolchains/<name>`) to view.
+ #[facet(args::positional)]
+ name: String,
+ },
}
#[derive(Facet)]
@@ -505,6 +512,7 @@
ToolchainAction::Log { name } => toolchain_log(&name, remote),
ToolchainAction::Export { name, dest } => toolchain_export(&name, &dest, remote),
ToolchainAction::Remove { name } => toolchain_remove(&name, remote),
+ ToolchainAction::View { name } => toolchain_view(&name, remote),
}
}
@@ -666,6 +674,25 @@
Ok(())
}
+/// Show `remote`'s toolchain `name`: its recipe/version/platform provenance
+/// and its on-disk footprint, computed by walking the git trees backing
+/// `bin`/`src` (see `git_toolchain::disk_usage`).
+fn toolchain_view(name: &str, remote: &str) -> Result<(), String> {
+ let refname = format!("{TOOLCHAINS_NS}/{name}");
+ sync(remote, &refname)?.ok_or_else(|| format!("no toolchain {name} on {remote}"))?;
+ let repo = repo()?;
+ let toolchain = git_toolchain::resolve(&repo, name).map_err(|error| error.to_string())?;
+ let usage = git_toolchain::disk_usage(&repo, name).map_err(|error| error.to_string())?;
+ let recipe = toolchain.recipe.as_deref().unwrap_or("hand-supplied");
+ println!(
+ "{name} {} {} {} {recipe}",
+ toolchain.version, toolchain.platform, toolchain.license
+ );
+ let printer = facet_pretty::PrettyPrinter::new().with_doc_comments(true);
+ println!("{}", usage.pretty_with(printer));
+ Ok(())
+}
+
/// Remove toolchain `name` on `remote`, deleting its ref and pushing the
/// update.
fn toolchain_remove(name: &str, remote: &str) -> Result<(), String> {
crates/git-toolchain/src/lib.rs
@@ -38,6 +38,7 @@
use gix::bstr::ByteSlice as _;
use gix::objs::tree::{Entry as TreeEntry, EntryKind, EntryMode};
use gix::objs::{FindExt as _, Tree, WriteTo as _};
+use gix::prelude::HeaderExt as _;
use gix_pack::data::input::Entry as PackEntry;
use rayon::prelude::*;
@@ -378,6 +379,65 @@
Ok(store.delete_ref(&toolchain_ref(name))?)
}
+/// A toolchain's on-disk footprint, in bytes, summed from the git trees
+/// backing it — `bin`'s tree when [`Bin::Embedded`] (`None` for
+/// [`Bin::Downloaded`], since those bytes live at the distributor's archives,
+/// not in this repository), and `src`'s tree, if imported.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Facet)]
+pub struct Usage {
+ /// Total bytes across every blob in `bin`'s tree, or `None` when `bin` is
+ /// [`Bin::Downloaded`] and so has no locally-stored tree to size.
+ pub bin_bytes: Option<u64>,
+ /// Total bytes across every blob in `src`'s tree, or `None` when no `src`
+ /// was imported.
+ pub src_bytes: Option<u64>,
+}
+
+/// Toolchain `name`'s on-disk footprint: recursively sums blob sizes in
+/// `bin`'s tree (when [`Bin::Embedded`]) and `src`'s tree (if present), read
+/// from object headers rather than fully decoding each blob.
+pub fn disk_usage(repo: &Path, name: &str) -> Result<Usage, Error> {
+ let toolchain = resolve(repo, name)?;
+ let odb = odb_at(repo)?;
+ let bin_bytes = match &toolchain.bin {
+ Bin::Embedded(tree) => Some(tree_size(&odb, tree.oid())?),
+ Bin::Downloaded(_) => None,
+ };
+ let src_bytes = toolchain
+ .src
+ .as_ref()
+ .map(|tree| tree_size(&odb, tree.oid()))
+ .transpose()?;
+ Ok(Usage {
+ bin_bytes,
+ src_bytes,
+ })
+}
+
+/// Recursively sum the byte size of every blob under `tree`, reading each
+/// object's header (kind + size) rather than fully decoding its content —
+/// cheap even for large binaries.
+fn tree_size(odb: &gix::odb::Handle, tree: ObjectId) -> Result<u64, Error> {
+ let mut buf = Vec::new();
+ let tree_ref = odb
+ .find_tree(&tree, &mut buf)
+ .map_err(|error| git_store::Error::Object(error.to_string()))?;
+ let mut total = 0u64;
+ for entry in &tree_ref.entries {
+ let size = match entry.mode.kind() {
+ EntryKind::Tree => tree_size(odb, entry.oid.to_owned())?,
+ EntryKind::Link | EntryKind::BlobExecutable | EntryKind::Blob => odb
+ .header(entry.oid)
+ .map_err(|error| git_store::Error::Object(error.to_string()))?
+ .size(),
+ // A submodule gitlink: no blob of its own to size in this repo.
+ EntryKind::Commit => 0,
+ };
+ total = total.saturating_add(size);
+ }
+ Ok(total)
+}
+
/// `refs/meta/toolchains/<name>`.
fn toolchain_ref(name: &str) -> String {
format!("{TOOLCHAINS_NS}/{name}")