git-ents.gitmain
⌘K
foforge
commit 60f5353
feat: 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

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

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}")