Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ jobs:
fail-fast: false
matrix:
include:
# Primary target: newest stable Elixir (1.19.x) on newest OTP (28.x).
# `erlef/setup-beam` resolves "1.19" to the latest 1.19.y.z that has
# an OTP-28 build on hex.pm. Owns the quality gates (format,
# credo, dialyzer, integration).
# Primary target: newest stable Elixir (1.20.x) on newest OTP (29.x).
# `erlef/setup-beam` resolves "1.20" to the latest 1.20.y.z that has
# an OTP-29 build. Owns the quality gates (format, credo, dialyzer,
# integration). Elixir 1.20 supports OTP 27-29.
- elixir: "1.20"
otp: "29"
primary: true
# Previous stable line — a compile + test smoke to catch regressions
# against 1.19/OTP-28. Quality gates run once, on the primary.
- elixir: "1.19"
otp: "28"
primary: true
primary: false
# Minimum-supported version — matches the `elixir: "~> 1.17"`
# constraint in mix.exs. If we ever need a 1.18+ feature,
# bump both the constraint and this matrix entry. Compile +
Expand Down Expand Up @@ -159,3 +164,34 @@ jobs:
CF_ARTIFACT_REMOTE: ${{ secrets.CF_ARTIFACT_REMOTE }}
CF_ARTIFACT_TOKEN: ${{ secrets.CF_ARTIFACT_TOKEN }}
run: mix test --warnings-as-errors --only cloudflare

consumer-smoke:
name: Consumer install smoke (package + use)
runs-on: ubuntu-24.04
# Separate from the `test` job on purpose: this builds the package
# exactly as it ships to Hex, depends on it from a throwaway project
# compiled in :prod, and uses the public API end-to-end. It catches
# missing `files:` entries and compile-time coupling to dev/test/
# optional deps — neither of which the in-repo suite can see.
# Live network (clones a public repo); runs on every push and PR.
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Elixir / OTP
uses: erlef/setup-beam@v1
with:
elixir-version: "1.19"
otp-version: "28"
version-type: loose

- name: Install local Hex & rebar
run: |
mix local.hex --force
mix local.rebar --force

- name: Fetch dependencies
run: mix deps.get

- name: Consumer-install smoke (clones elixir-ai-tools/just_bash)
run: bash scripts/consumer_smoke.sh
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ ex_git-*.tar
# Temporary files, for example, from tests.
/tmp/

# Local scratch: internal notes, third-party product critiques, repro
# scripts. Never published to GitHub or hex.
/scratch/

# Environment / secrets. Never commit.
.env
.env.*
Expand All @@ -33,3 +37,6 @@ ex_git-*.tar
# mix dialyzer --plt; never commit them.
priv/plts/


# hex.build package artifact
/exgit-*.tar
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.19.5-otp-28
erlang 28.4
erlang 29.0.2
elixir 1.20.2-otp-29
72 changes: 62 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,65 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.1.0] — 2026-07-01

Initial release: pure-Elixir git client for clone, fetch, push over
smart HTTP v2, with lazy partial-clone support and a path-oriented FS
API for agents.

See [README](./README.md) and [BENCHMARKS on the smoketest
repo](https://github.com/ivarvong/exgit_smoketest/blob/main/BENCHMARKS.md).

### Security — credential redaction + ref bounds

- **Telemetry no longer leaks URL-embedded credentials.** A token
embedded in a remote URL (`https://token@host/…`, as git clients
commonly accept) is now redacted to `***` before the URL enters any
`:telemetry` span metadata (`ls_refs`, `fetch`, `push`) or the
`[:exgit, :security, :ref_rejected]` event. Previously such a token
could reach telemetry exporters / log aggregators. Prefer the
`:auth` field regardless — it was already redacted.
- **`ls-refs` responses are capped at 1,000,000 refs.** A hostile or
broken server can no longer stream unbounded refs into client
memory; the transport stream halts once the cap trips and the
caller gets `{:error, {:too_many_refs, cap}}`. Tunable via the
`:max_refs` option on `Exgit.Transport.HTTP.new/2`. Real repos
(linux, esp-idf) sit far below the default.
- **Dependencies bumped to clear HTTP-stack advisories.** `req`
`0.5.17 → 0.6.2` and `mint` `1.7.1 → 1.9.0` resolve the decompression-
bomb DoS (CVE-2026-49755), multipart header injection
(CVE-2026-49756), and HTTP/2 CONTINUATION flood (CVE-2026-49754).
The cross-origin credential-leak test suite was re-run against the
new `req` line. The only remaining `mix hex.audit` advisories are in
`cowlib`, reachable solely through the `only: :test` `bypass`
dependency — never part of the published package or a consumer's
runtime.

### Notes

- The optional `:vfs` integration depends on the pre-1.0 `vfs`
package (`~> 0.1.0`), which itself depends on exgit. The cycle is
handled with `runtime: false` + a compile-time `Code.ensure_loaded?`
guard, but treat the integration as pre-release and pin `vfs` if you
rely on it.

### Added — size-aware reads

- **`Exgit.FS.size/3`** — byte size of a blob at a path *without*
materializing its content. The size-aware companion to
`read_path/4`: gate on it before pulling a blob into memory.
O(1) for the in-memory store; on-disk loose objects inflate only
the header. Resolving the path may fetch trees (small) on a lazy
clone, but never the blob — an un-fetched blob returns
`{:error, :not_local}` instead of triggering a possibly-multi-GB
fetch. Directories return `{:error, :not_a_blob}`; gitlink
(submodule) entries return `{:error, :submodule}` — as do
`read_path/4` lookups on them, while `stat/3` reports
`%{type: :submodule}` without fetching.
- **`Exgit.ObjectStore.object_size/2`** — new protocol callback
backing the above. Memory keeps a parallel `sha => size` index
(no extra decompression); `Promisor` answers from cache or
returns `{:error, :not_local}` without fetching.

### Added — observability + workload bench

Expand All @@ -18,7 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`Exgit.Repository.memory_report/1`** — structured memory
report (object counts by type, cache_bytes, max_cache_bytes,
mode, backend) with consistent shape across all object-store
backends. Suitable for emission into observability stacks.
backends; counts report `:unknown` for backends that can't be
introspected (Disk). Suitable for emission into observability
stacks.
- **`bench/agent_workload.exs`** — realistic agent-session
benchmark: clone + prefetch + ls + grep + reads, with `:cold`
and `:hot` variants. Reports per-op breakdown + peak cache
Expand Down Expand Up @@ -392,11 +452,3 @@ taking the opportunity to land the right shapes before v0.1.
corrupt or tampered objects return tagged errors instead of
crashing.

## [0.1.0] — 2026-04-17

Initial release: pure-Elixir git client for clone, fetch, push over
smart HTTP v2, with lazy partial-clone support and a path-oriented FS
API for agents.

See [README](./README.md) and [BENCHMARKS on the smoketest
repo](https://github.com/ivarvong/exgit_smoketest/blob/main/BENCHMARKS.md).
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,14 @@ What's in place:
eviction for long-running agent loops.
- **Protocol v2 `symrefs`** — `Exgit.clone/2` picks the server's
actual HEAD target instead of guessing main/master.
- **399 tests, 29 properties, 0 failures** across default, slow,
real_git, and live-integration tiers.
- **CI gates**: Elixir 1.17 / OTP 27 on ubuntu-24.04 with
warnings-as-errors, Credo, Dialyzer, format check, partial-clone
roundtrip against GitHub.
- **870 tests (52 properties), 0 failures** across the default,
slow, and real_git tiers; live-integration and authenticated
tiers run in CI on top.
- **CI gates**: three-tier matrix on ubuntu-24.04 — Elixir 1.20 /
OTP 29 (primary: warnings-as-errors, Credo, Dialyzer, format
check, partial-clone roundtrip against GitHub), plus 1.19 / OTP 28
and 1.17 / OTP 27 (minimum-supported) compile-and-test tiers, and
a consumer-install smoke test of the built hex package.

See [CHANGELOG.md](./CHANGELOG.md) for details and
[SECURITY.md](./SECURITY.md) for the threat model.
Expand Down Expand Up @@ -370,7 +373,9 @@ changes require a major-version bump.

Functions annotated `@doc experimental: true` — currently
`FS.prefetch/3` and `Repository.materialize/2` — are explicitly
exempt from SemVer guarantees until marked stable. The `:lazy` and
exempt from SemVer guarantees until marked stable. The whole
`Exgit.Index` module is likewise experimental (see its `@moduledoc`)
and may change in any release. The `:lazy` and
`:filter` options on `Exgit.clone/2` are also experimental: the
threading contract (`{:ok, result, repo}` shape) may evolve before
v1.0.
Expand Down
Loading
Loading