git-remote-object-store 0.2.3

Git remote helper backed by cloud object stores (S3, Azure Blob Storage)
Documentation
[workspace]
members = [".", "cli", "xtask"]
# Exclude xtask from the default `cargo build` / `cargo test` set so a plain
# `cargo build` does not pull a second binary into the workspace target dir.
# Invoke it explicitly with `cargo xtask install` (see `.cargo/config.toml`).
default-members = [".", "cli"]
resolver = "2"

# Single source of truth for the workspace MSRV. The CLI and xtask crates
# inherit this via `rust-version.workspace = true`, and the CI / release
# workflows read it from `cargo metadata` so a bump here propagates
# everywhere without manual edits.
[workspace.package]
rust-version = "1.94"

[package]
name = "git-remote-object-store"
version = "0.2.3"
edition = "2024"
rust-version.workspace = true
license = "Apache-2.0"
repository = "https://github.com/dekobon/git-remote-object-store"
authors = ["Elijah Zupancic <elijah@zupancic.name>"]
description = "Git remote helper backed by cloud object stores (S3, Azure Blob Storage)"
readme = "README.md"
keywords = ["git", "remote-helper", "s3", "azure", "object-store"]
categories = ["command-line-utilities", "development-tools"]

[lints.rust]
missing_docs = "warn"

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
# `unreachable!()` expands to `panic!()`, which `.claude/rules/rust.md`
# bans in non-test code. Encoding the rule as a lint catches future
# regressions at compile time. Test code that legitimately needs
# `unreachable!()` (none today) must opt out with `#[allow(...)]`.
unreachable = "deny"

[features]
# Expose test-only helpers (notably `object_store::mock::MockStore`) to
# integration tests in `tests/` and to higher-phase test code that lives
# outside the lib crate. Production builds leave the feature off; in-crate
# unit tests pick up the helpers via `cfg(test)` regardless of the feature.
test-util = []

[dependencies]
# Async traits via Boxed futures. `async_trait` keeps `dyn ObjectStore +
# Send + Sync` ergonomic; native AFIT (stable since 1.75) is awkward
# through `dyn` because each method's future must be `Send`-bounded.
async-trait = "0.1"

# Async runtime
tokio = { version = "1", features = [
  "rt-multi-thread",
  "macros",
  "fs",
  "io-std",
  "io-util",
  "process",
  "signal",
  "time",
] }

# Errors
thiserror = "2"
anyhow = "1"

# Logging
tracing = "0.1"
# `reload` is required for runtime verbosity flips driven by the helper
# protocol's `option verbosity <n>` line.
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Time
time = { version = "0.3", features = ["parsing", "formatting", "macros"] }

# JSON (LFS)
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Interactive prompts for the management CLI (`git-remote-object-store`).
# Default features pull in fuzzy-select / completion / history machinery
# that the `Select` + `Confirm` flows here do not exercise; disabling them
# keeps the dependency footprint tight.
dialoguer = { version = "0.11", default-features = false }

# UUID v4 for the doctor's quarantine ref suffix
# (`<ref>_<uuid8>`).
uuid = { version = "1", features = ["v4"] }

# URL parsing
url = "2"

# Git operations (gitoxide). Sub-crates are kept on the same versions
# `gix` 0.83 itself selects to avoid duplicate copies in the dep graph.
#
# `gix` is pulled in with default features on purpose: `gix-hash` 0.25's
# `Kind` enum has its `Sha1` / `Sha256` variants gated behind the matching
# crate features, and its own default feature set is empty. The explicit
# `features = ["sha1"]` below ensures `Kind` is inhabited; disabling
# `gix`'s defaults causes the rest of the gix tree to be compiled without
# `sha1` and reintroduces the non-exhaustive-match build error in
# gix-hash. Keep the defaults until we have a reason to slim the tree.
gix = "0.83"
gix-hash = { version = "0.25", features = ["sha1"] }
gix-pack = "0.70"
gix-validate = "0.11"
gix-archive = "0.32"

# Bytes / IO
bytes = "1"
tempfile = "3"

# AWS SDK for the S3 backend. `aws-config` carries the
# `behavior-version-latest` feature so the SDK picks up its own latest
# defaults automatically; revisit pinning later if reproducibility
# becomes a concern.
#
# `credentials-login` enables IAM Identity Center (SSO) login-session
# credential providers. Without it, profiles that use `sso-session` or
# a `LoginSession` chain silently fail to provide credentials, the SDK
# falls through to the EC2 IMDS metadata endpoint, and the resulting
# network error is misreported as "invalid credentials network error".
#
# `aws-sdk-s3` defaults include the legacy `rustls` feature, which
# routes through `aws-smithy-runtime/tls-rustls` and
# `aws-smithy-http-client/legacy-rustls-ring`, pinning `rustls 0.21` +
# `rustls-webpki 0.101.x` — both of which carry open RUSTSEC advisories
# (GHSA-4p46-pwfr-66x6 high-severity panic on malformed CRL, plus two
# name-constraint advisories). The modern `default-https-client` feature
# uses `rustls 0.23` / `rustls-webpki 0.103.x` instead. Disable defaults
# and re-enable everything except the legacy `rustls` feature.
aws-config = { version = "1", features = [
  "behavior-version-latest",
  "credentials-login",
] }
aws-sdk-s3 = { version = "1", default-features = false, features = [
  "sigv4a",
  "http-1x",
  "default-https-client",
  "rt-tokio",
] }

# Direct dep on the smithy HTTP client so `S3Store::from_remote_url`
# can install a custom connector with `pool_idle_timeout(30s)`. The
# default-built client keeps idle connections in the pool indefinitely,
# which wedges long-running LFS sessions on rotated VIPs (#26 / #27).
# `rustls-aws-lc` is the same TLS provider feature `aws-sdk-s3`'s
# `default-https-client` enables transitively (via
# `aws-smithy-runtime/default-https-client`), so cargo unifies on a
# single rustls 0.23 + aws-lc-rs stack — matching the existing tree.
#
# After bumping `aws-config` or `aws-sdk-s3`, run:
#   cargo tree -e normal -d | rg rustls
# A duplicate rustls major (or any rustls 0.21 entry) means the SDK's
# `default-https-client` selected a different provider; update the
# `CryptoMode` in `src/object_store/s3.rs` to match.
aws-smithy-http-client = { version = "1", features = ["rustls-aws-lc"] }

# Bridges `aws_smithy_types::DateTime` → `time::OffsetDateTime` for the
# `ObjectMeta::last_modified` conversion.
aws-smithy-types-convert = { version = "0.60", features = ["convert-time"] }

# Required for `StreamExt::next` on `aws_smithy_types::byte_stream::ByteStream`
# in the small-object GET path and the multipart download orchestrator.
futures = "0.3"

# `x-amz-copy-source` is forwarded verbatim by aws-sdk-s3 — keys with
# reserved characters (notably `#` in `LOCK#.lock`) must be percent-encoded
# before being passed in.
percent-encoding = "2"

# Azure SDK for the Blob backend. The SDK is in beta (0.12)
# and does NOT support shared-key authentication out of the box — only
# `Arc<dyn TokenCredential>` (Entra ID). We implement a custom shared-key
# signing `Policy` ourselves so users can authenticate with account keys
# (the only auth Azurite supports without HTTPS + OAuth) and so the
# documented `AZSTORE_<NAME>_KEY` / `AZSTORE_<NAME>_CONNECTION_STRING`
# credential aliases work. Tracking issue: Azure/azure-sdk-for-rust#2975.
azure_core = "0.35"
azure_storage_blob = "0.12"
azure_identity = "0.35"

# Required for the shared-key signing policy: HMAC-SHA256 over the
# Azure canonicalized string-to-sign, base64-encoded for the
# `Authorization: SharedKey <account>:<sig>` header.
hmac = "0.12"
sha2 = "0.10"
base64 = "0.22"

# HTTP transport for the Azure Blob backend. `azure_core` 0.35's default
# transport pools connections forever and never sets TCP keepalive, so a
# rotated VIP can hang a long-running LFS session until the OS-level TCP
# timeout fires (issue #26). We build our own `reqwest::Client` with
# bounded `pool_idle_timeout` / `tcp_keepalive` and install it via
# `ClientOptions::transport`. `rustls` keeps HTTPS to
# `*.blob.core.windows.net` explicit rather than relying on
# `azure_core`'s transitive feature set; cargo unifies with the
# additional `gzip`/`deflate`/`stream` features `azure_core` already
# enables on its own `reqwest` dep, so no decompression/streaming
# capability is lost.
reqwest = { version = "0.13", default-features = false, features = [
  "http2",
  "rustls",
] }

[dev-dependencies]
# Property-based testing for URL round-trip parse/format.
proptest = "1"

# `test-util` exposes `capture_request`, which the `MultipartUploadGuard`
# Drop-fires-AbortMultipartUpload test (#173) wires into the SDK's
# `http_client` slot to byte-equality-check the request the detached
# abort task issues. The crate itself is already a regular dep (above);
# this dev-only entry only flips the `test-util` feature on for test
# builds.
aws-smithy-http-client = { version = "1", features = ["test-util"] }

# `tokio` `test-util` feature gates `tokio::time::pause`, `advance`, and
# `#[tokio::test(start_paused = true)]`. Issue #118's lock-heartbeat
# tests advance virtual time across multiple TTLs without actually
# sleeping, which would otherwise force the test suite to wait minutes
# of wall time per heartbeat assertion. Production builds do not see
# this feature — it's dev-only.
tokio = { version = "1", features = ["test-util"] }

# zlib compression for synthesised pack bytes in unit tests. Production
# code only needs `Decompress` (provided by `gix::features::zlib`); the
# delta-depth regression test in `packchain::read::tests` constructs a
# valid pack entry payload at test time, which requires the deflate
# direction. `flate2` is already in the transitive graph via `gix-pack`.
flate2 = "1"

# RustFS testcontainer used by `tests/s3_store_integration.rs` (gated on the
# `integration-s3` Cargo feature). Cargo does not allow `optional = true` on
# dev-dependencies, so this compiles for every `cargo test` invocation but
# only links into the integration test file when the feature is enabled.
# Features:
#   - `blocking` enables `SyncRunner`, which starts the container outside of
#     any tokio runtime so it can be shared across multiple `#[tokio::test]`
#     -spawned runtimes without their dispatch tasks tearing each other down.
#   - `http_wait_plain` enables `WaitFor::http(...)`, required because RustFS
#     writes its startup logs to a log file inside the container rather than
#     to stdout, so a log-message wait would never fire. We poll the S3
#     endpoint instead.
#
# The fixture uses `testcontainers::GenericImage` directly with a pinned
# RustFS tag rather than `testcontainers-modules`, which hardcodes `:latest`
# and would silently drift across alpha releases.
testcontainers = { version = "0.27", features = [
  "blocking",
  "http_wait_plain",
] }

# Note: `reqwest` is a regular dependency (above) — used in production
# by the Azure backend's transport tuning (#26 / #28) and reused here by
# `tests/azure_store_integration.rs` (gated on `integration-azure`) for
# the one-off Create-Container setup request signed via our shared-key
# policy. `sha2` is similarly a runtime dep (the shared-key signing
# policy) so the integration tests can reuse the same crate version
# for byte-equality assertions on the multipart-download path.

# Keep line tables in release builds so dsymutil/objcopy can produce
# meaningful backtraces from a released binary. Release CI splits
# these into separate debug-symbol artefacts and strips the primary
# binary before shipping; local `cargo install` users get a slightly
# larger binary with line-level panic backtraces.
[profile.release]
debug = "line-tables-only"