Exgit.Transport.HTTP (exgit v0.1.0)

Copy Markdown View Source

Exgit.Transport implementation for git smart-HTTP v2.

Speaks git protocol v2 over HTTPS using Req as the HTTP client. Implements capabilities/1, ls_refs/2, fetch/3, and push/4; supports want-ref, filter, sideband-all, and symrefs extensions.

Credentials

The :auth field accepts either a bare auth tuple (auto-wrapped in %Exgit.Credentials{} with host-binding) or an explicit %Exgit.Credentials{} struct. Host-bound credentials refuse to emit auth headers when the URL host doesn't match the bound pattern, defending against cross-origin leaks through redirects or user-supplied URLs.

TLS

verify_tls: true (default) uses :public_key.cacerts_get/0 and enables hostname verification via :ssl.pkix_verify_hostname/3.

Redirects

Disabled by default to prevent any chance of credential leaks through an unexpected redirect target. Opt-in via redirect: :same_origin or redirect: :follow; host-binding on :auth remains in force either way.

Summary

Functions

Compute the auth headers for a specific request URL. Exposed for testing; production call-sites use request_opts/5 or do_request/5.

Discover the server's protocol-v2 capability advertisement.

Return {capabilities, updated_transport} so the caller can thread the transport forward and avoid re-discovering on every call.

Fetch a pack for the given wants (20-byte SHAs) via the protocol-v2 fetch command.

List the remote's refs via the protocol-v2 ls-refs command.

Build a transport for the git smart-HTTP remote at url.

Push ref updates and the accompanying pack_bytes via git-receive-pack.

Build the keyword list we'd pass to Req.request/1 for the given request. Exposed publicly for test introspection; production code goes through do_request/5.

Types

auth()

@type auth() :: auth_value() | Exgit.Credentials.t()

auth_value()

@type auth_value() ::
  nil
  | {:basic, String.t(), String.t()}
  | {:bearer, String.t()}
  | {:header, String.t(), String.t()}
  | {:callback, (String.t() -> [{String.t(), String.t()}])}

t()

@type t() :: %Exgit.Transport.HTTP{
  auth: auth(),
  capabilities_cache: term(),
  connect_options: term(),
  connect_timeout: term(),
  max_refs: term(),
  receive_timeout: term(),
  redirect: term(),
  url: String.t(),
  user_agent: String.t(),
  verify_tls: term()
}

Functions

auth_headers_for(http, url)

@spec auth_headers_for(t(), String.t()) :: [{String.t(), String.t()}]

Compute the auth headers for a specific request URL. Exposed for testing; production call-sites use request_opts/5 or do_request/5.

This is the enforcement point for credential host-binding: a %Exgit.Credentials{} with a non-matching host pattern returns [] regardless of what the caller thought they attached.

capabilities(t)

@spec capabilities(t()) :: {:ok, map()} | {:error, term()}

Discover the server's protocol-v2 capability advertisement.

Performs the info/refs?service=git-upload-pack discovery GET and parses the advertisement into a map keyed by capability name (string keys; well-known entries like :version get structured values). Returns {:error, :server_does_not_support_v2} for protocol-v1-only servers, {:error, {:malformed_response, reason}} when the body is not valid pkt-line framing.

A struct carrying a memoized capabilities_cache (see capabilities_cached/1) returns the cached result without HTTP.

capabilities_cached(t)

@spec capabilities_cached(t()) :: {term(), t()}

Return {capabilities, updated_transport} so the caller can thread the transport forward and avoid re-discovering on every call.

The default fetch/ls-refs paths still accept a non-cached transport (to preserve backward compatibility with struct-sharing callers); capabilities_cached/1 is the opt-in path for workflows that make many small fetches against the same transport.

fetch(t, wants, opts \\ [])

@spec fetch(t(), [binary()], keyword()) :: {:ok, binary(), map()} | {:error, term()}

Fetch a pack for the given wants (20-byte SHAs) via the protocol-v2 fetch command.

Returns {:ok, pack_bytes, summary}. With :object_store the pack is stream-parsed straight into the store — pack_bytes is <<>> and summary carries :objects and the updated :store. Without it, pack_bytes is the raw pack for the caller to parse. Sideband channel-3 server messages surface as {:error, {:server_error, msg}}.

Options

  • :haves — SHAs the client already has, for negotiation.
  • :depth — shallow-clone depth (deepen).
  • :filter — partial-clone filter spec (e.g. "blob:none").
  • :object_store — an Exgit.ObjectStore to stream objects into as they arrive (bounded memory on multi-GB packs).
  • :sideband, :thin_pack, :ofs_delta — explicit feature overrides; by default they're negotiated from the server's advertised capabilities.

ls_refs(t, opts \\ [])

@spec ls_refs(
  t(),
  keyword()
) ::
  {:ok, [Exgit.Transport.ref_entry()], Exgit.Transport.ls_refs_meta()}
  | {:error, term()}

List the remote's refs via the protocol-v2 ls-refs command.

Returns {:ok, refs, meta} where refs is a list of {ref_name, sha} 2-tuples and meta carries protocol-v2 side-channel data (:head symref target, :peeled tag targets) — see Exgit.Transport.ls_refs_meta/0. Invalid ref names from the server are dropped (with [:exgit, :security, :ref_rejected] telemetry); more than max_refs refs aborts with {:error, {:too_many_refs, cap}}.

Options

  • :prefix — ref-prefix filter or list of filters (e.g. "refs/heads/"); default [] (all refs).
  • :symrefs — ask for symref targets, revealing where HEAD points (default true).
  • :peeled — ask for peeled:<sha> attributes on annotated tags (default true).

new(url, opts \\ [])

@spec new(
  String.t(),
  keyword()
) :: t()

Build a transport for the git smart-HTTP remote at url.

Options

  • :auth — credentials; a bare auth tuple (auto-wrapped in a host-bound %Exgit.Credentials{}) or an explicit %Exgit.Credentials{} struct. See the module doc.
  • :user_agent — the user-agent header value.
  • :connect_timeout — connect timeout in milliseconds (default 10_000).
  • :receive_timeout — response-body timeout in milliseconds (default 300_000); :infinity disables.
  • :verify_tls — TLS peer verification (default true).
  • :connect_options — extra transport options merged into the library's TLS defaults (custom CA bundles, mTLS, SNI).
  • :redirect — redirect policy: false (default), :same_origin, or :follow.
  • :max_refs — cap on refs accepted from a single ls-refs response (default 1000000). ls_refs/2 aborts with {:error, {:too_many_refs, cap}} when a server exceeds it — a backstop against a hostile server streaming unbounded refs into client memory.

push(t, updates, pack_bytes, opts \\ [])

@spec push(t(), [Exgit.Transport.ref_update()], binary(), keyword()) ::
  {:ok, %{ref_results: [{String.t(), :ok | :error}]}} | {:error, term()}

Push ref updates and the accompanying pack_bytes via git-receive-pack.

Each update is a {ref, old_sha, new_sha} 3-tuple (20-byte SHAs; nil means the all-zero SHA, i.e. create/delete). Returns {:ok, %{ref_results: [{ref, :ok | :error}]}} parsed from the server's report-status response, or {:error, {:malformed_response, reason}} when the report is not valid pkt-line framing.

request_opts(t, method, url, headers, body)

@spec request_opts(
  t(),
  atom(),
  String.t(),
  [{String.t(), String.t()}],
  binary() | nil
) :: keyword()

Build the keyword list we'd pass to Req.request/1 for the given request. Exposed publicly for test introspection; production code goes through do_request/5.