From 902db289ad7f138f30b297ab143a4204111b458d Mon Sep 17 00:00:00 2001 From: Ivar Vong Date: Thu, 2 Jul 2026 08:11:24 -0400 Subject: [PATCH 1/2] docs: add runnable partial-clone worked example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit examples/partial_clone_demo.exs — Mix.install of the published package, blob:none partial clone of a public repo, one file read, and a memory_report/1 comparison against an eager clone proving exactly one blob crossed the wire (1 of 1,906 blobs; 358 KB of 5.7 MB compressed, measured against GitHub). Also demos the FS.size/3 gate: {:error, :not_local} before the read (no fetch triggered), O(1) from cache after. README links it from the partial-clone section; .formatter.exs now covers examples/. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VEo6srWr3aFwhtwaXDTCZZ --- .formatter.exs | 2 +- README.md | 9 +++++ examples/partial_clone_demo.exs | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/partial_clone_demo.exs diff --git a/.formatter.exs b/.formatter.exs index d2cda26..c185fe1 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,4 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test,examples}/**/*.{ex,exs}"] ] diff --git a/README.md b/README.md index 9d22121..5528b45 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,15 @@ refetch. Perfect for agent loops that want snapshot semantics. The on-disk format is a standard bare git repo — `git log`, `git fsck`, and friends all work on it. +There's a runnable worked example at +[`examples/partial_clone_demo.exs`](examples/partial_clone_demo.exs) — +`Mix.install`, partial clone, one file read, and a `memory_report/1` +comparison proving exactly one blob crossed the wire: + +```sh +elixir examples/partial_clone_demo.exs +``` + ### Private repos ```elixir diff --git a/examples/partial_clone_demo.exs b/examples/partial_clone_demo.exs new file mode 100644 index 0000000..5057816 --- /dev/null +++ b/examples/partial_clone_demo.exs @@ -0,0 +1,66 @@ +# Partial clone, worked example. +# +# Clones a public repo under a `blob:none` filter — the server ships +# refs + commits + trees but NO file contents — then reads one file +# and proves, via `Exgit.Repository.memory_report/1`, that exactly +# one blob was pulled over the network. +# +# Run with: elixir examples/partial_clone_demo.exs + +Mix.install([{:exgit, "~> 0.1.0"}]) + +url = "https://github.com/elixir-ai-tools/just_bash" +path = "README.md" + +fmt = fn n -> n |> Integer.to_string() |> String.replace(~r/(\d)(?=(\d{3})+$)/, "\\1,") end + +report = fn label, r -> + IO.puts( + " #{String.pad_trailing(label, 24)} " <> + "objects: #{String.pad_leading(fmt.(r.object_count), 6)} " <> + "blobs: #{String.pad_leading(fmt.(r.blob_count), 6)} " <> + "trees: #{String.pad_leading(fmt.(r.tree_count), 4)} " <> + "cache: #{String.pad_leading(fmt.(r.cache_bytes), 9)} B" + ) +end + +# ── 1. Partial clone: no blobs cross the wire ────────────────────── +{:ok, repo} = Exgit.clone(url, filter: {:blob, :none}) +IO.puts("\nPartial clone (blob:none) of #{url}:") +report.("after clone", Exgit.Repository.memory_report(repo)) + +# ── 2. Size probe: answered without fetching ─────────────────────── +# The blob isn't local and size/3 refuses to trigger a fetch for it. +{:error, :not_local} = Exgit.FS.size(repo, "HEAD", path) +IO.puts("\n FS.size(#{inspect(path)}) -> {:error, :not_local} (no fetch triggered)") + +# ── 3. Read ONE file: exactly one blob is fetched ─────────────────── +{:ok, {_mode, blob}, repo} = Exgit.FS.read_path(repo, "HEAD", path) +first_line = blob.data |> String.split("\n", parts: 2) |> hd() + +IO.puts( + " FS.read_path(#{inspect(path)}) -> #{byte_size(blob.data)} bytes: #{inspect(first_line)}" +) + +# Now it's cached: size answers in O(1), still nothing new fetched. +{:ok, size, repo} = Exgit.FS.size(repo, "HEAD", path) +IO.puts(" FS.size(#{inspect(path)}) -> {:ok, #{size}} (from cache)\n") + +report.("after reading one file", Exgit.Repository.memory_report(repo)) + +# ── 4. The comparison: what a full clone hauls in ─────────────────── +{:ok, eager} = Exgit.clone(url) +full = Exgit.Repository.memory_report(eager) +file_count = Exgit.FS.walk(eager, "HEAD") |> Enum.count() + +IO.puts("\nEager clone of the same repo (#{fmt.(file_count)} files at HEAD):") +report.("full object graph", full) + +partial = Exgit.Repository.memory_report(repo) + +IO.puts(""" + +The partial clone fetched #{fmt.(partial.blob_count)} of #{fmt.(full.blob_count)} blobs \ +(#{fmt.(partial.cache_bytes)} of #{fmt.(full.cache_bytes)} compressed bytes) \ +to read #{inspect(path)}. +""") From 5b9cd116b2ab0523677e3ba1a07417bcae186190 Mon Sep 17 00:00:00 2001 From: Ivar Vong Date: Thu, 2 Jul 2026 10:41:40 -0400 Subject: [PATCH 2/2] docs: slim the worked example to one clone, one beat per idea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Print plumbing (thousands-separator regex, padded-column report) replaced with Map.take + IO.inspect; the eager-clone control group dropped — the blob_count: 0 -> 1 transition around the single read is the proof, and a free FS.ls afterward shows navigation doesn't move it. 67 lines -> 32, one network clone + one blob fetch. (FS.walk was considered for the file-count denominator but correctly refuses :lazy repositories; ls has no such guard since it reads one local tree.) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VEo6srWr3aFwhtwaXDTCZZ --- README.md | 4 +- examples/partial_clone_demo.exs | 70 +++++++++------------------------ 2 files changed, 20 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 5528b45..edd18dc 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ The on-disk format is a standard bare git repo — `git log`, `git fsck`, and fr There's a runnable worked example at [`examples/partial_clone_demo.exs`](examples/partial_clone_demo.exs) — -`Mix.install`, partial clone, one file read, and a `memory_report/1` -comparison proving exactly one blob crossed the wire: +`Mix.install`, partial clone, one file read, with `memory_report/1` +showing `blob_count: 0 -> 1` around the read: ```sh elixir examples/partial_clone_demo.exs diff --git a/examples/partial_clone_demo.exs b/examples/partial_clone_demo.exs index 5057816..fa19b90 100644 --- a/examples/partial_clone_demo.exs +++ b/examples/partial_clone_demo.exs @@ -1,66 +1,32 @@ # Partial clone, worked example. # # Clones a public repo under a `blob:none` filter — the server ships -# refs + commits + trees but NO file contents — then reads one file -# and proves, via `Exgit.Repository.memory_report/1`, that exactly -# one blob was pulled over the network. +# refs + commits + trees but NO file contents — then reads one file. +# The `blob_count: 0 -> 1` transition around the read is the proof +# that exactly one blob crossed the wire. # # Run with: elixir examples/partial_clone_demo.exs Mix.install([{:exgit, "~> 0.1.0"}]) -url = "https://github.com/elixir-ai-tools/just_bash" -path = "README.md" - -fmt = fn n -> n |> Integer.to_string() |> String.replace(~r/(\d)(?=(\d{3})+$)/, "\\1,") end - -report = fn label, r -> - IO.puts( - " #{String.pad_trailing(label, 24)} " <> - "objects: #{String.pad_leading(fmt.(r.object_count), 6)} " <> - "blobs: #{String.pad_leading(fmt.(r.blob_count), 6)} " <> - "trees: #{String.pad_leading(fmt.(r.tree_count), 4)} " <> - "cache: #{String.pad_leading(fmt.(r.cache_bytes), 9)} B" - ) +stats = fn repo -> + Exgit.Repository.memory_report(repo) |> Map.take([:blob_count, :tree_count, :cache_bytes]) end -# ── 1. Partial clone: no blobs cross the wire ────────────────────── -{:ok, repo} = Exgit.clone(url, filter: {:blob, :none}) -IO.puts("\nPartial clone (blob:none) of #{url}:") -report.("after clone", Exgit.Repository.memory_report(repo)) +# Partial clone: refs + commits + trees cross the wire. No file contents. +{:ok, repo} = Exgit.clone("https://github.com/elixir-ai-tools/just_bash", filter: {:blob, :none}) +IO.inspect(stats.(repo), label: "after clone ") -# ── 2. Size probe: answered without fetching ─────────────────────── -# The blob isn't local and size/3 refuses to trigger a fetch for it. -{:error, :not_local} = Exgit.FS.size(repo, "HEAD", path) -IO.puts("\n FS.size(#{inspect(path)}) -> {:error, :not_local} (no fetch triggered)") +# The size probe refuses to fetch — gate on it before pulling big files. +{:error, :not_local} = Exgit.FS.size(repo, "HEAD", "README.md") -# ── 3. Read ONE file: exactly one blob is fetched ─────────────────── -{:ok, {_mode, blob}, repo} = Exgit.FS.read_path(repo, "HEAD", path) +# Reading ONE file fetches ONE blob. +{:ok, {_mode, blob}, repo} = Exgit.FS.read_path(repo, "HEAD", "README.md") first_line = blob.data |> String.split("\n", parts: 2) |> hd() +IO.puts(~s(read README.md: #{byte_size(blob.data)} bytes — "#{first_line}")) +IO.inspect(stats.(repo), label: "after one read") -IO.puts( - " FS.read_path(#{inspect(path)}) -> #{byte_size(blob.data)} bytes: #{inspect(first_line)}" -) - -# Now it's cached: size answers in O(1), still nothing new fetched. -{:ok, size, repo} = Exgit.FS.size(repo, "HEAD", path) -IO.puts(" FS.size(#{inspect(path)}) -> {:ok, #{size}} (from cache)\n") - -report.("after reading one file", Exgit.Repository.memory_report(repo)) - -# ── 4. The comparison: what a full clone hauls in ─────────────────── -{:ok, eager} = Exgit.clone(url) -full = Exgit.Repository.memory_report(eager) -file_count = Exgit.FS.walk(eager, "HEAD") |> Enum.count() - -IO.puts("\nEager clone of the same repo (#{fmt.(file_count)} files at HEAD):") -report.("full object graph", full) - -partial = Exgit.Repository.memory_report(repo) - -IO.puts(""" - -The partial clone fetched #{fmt.(partial.blob_count)} of #{fmt.(full.blob_count)} blobs \ -(#{fmt.(partial.cache_bytes)} of #{fmt.(full.cache_bytes)} compressed bytes) \ -to read #{inspect(path)}. -""") +# Navigation stays free: directory listings come from the local +# trees, so blob_count doesn't move. +{:ok, entries, repo} = Exgit.FS.ls(repo, "HEAD", "") +IO.puts("ls /: #{length(entries)} entries — blob_count still #{stats.(repo).blob_count}")