Skip to content

Release v0.6.0

Choose a tag to compare

@xfbs xfbs released this 13 May 01:00
· 47 commits to master since this release
9da141f

Fixed

  • api: ApiError::Status Display now surfaces the server's body
    message verbatim when present, falling back to
    Authorization error: <url> (401/403) only when the body is empty.
    Previously the body was dropped for 401/403, hiding messages like
    Expected ref "refs/heads/other", got "refs/heads/main" that
    t-pre-push / t-fetch-refspec / t-push / t-credentials all
    grep for. Lands 5 tests (3 near-misses across 3 suites, plus 2
    bonus from suites that shared the same root cause).
  • creds: HelperChain::fill now skips helpers that error and
    continues to the next, matching upstream's CredentialHelpers.Fill
    (creds/creds.go:502). Previously a failed askpass program
    short-circuited the chain before git credential got a turn,
    so a missing GIT_ASKPASS would lock the user out instead of
    falling through to the configured credential helper.
  • creds: emit creds: failed to find GIT_ASKPASS command: <prog>
    when the askpass executable isn't on PATH, and
    creds: git credential <sub> (<proto>, <host>, <path>) on every
    git credential invocation. Both match upstream's tracerx.Printf
    format at creds/creds.go:284 / :328. Lands
    t-credentials-no-prompt::askpass: push with bad askpass.
  • git lfs fetch <ref>... now scans only the HEAD-state of each
    named ref instead of walking its full history. Historical /
    deleted-from-HEAD pointers still get fetched via --all or
    --recent. Matches upstream's fetchRef vs fetchRefs split
    and is a prerequisite for the upcoming --recent semantics.

Added

  • migrate info --fixup now does the real per-tree attribute walk:
    list every blob at the selected ref, build a fresh AttrSet from
    that tree's .gitattributes files (root + nested), and count any
    non-attrs, non-symlink, non-pointer blob whose path is LFS-tracked
    per the attrs. Mirrors upstream's
    commands/command_migrate_info.go::BlobFn fixup branch. Lands
    t-migrate-info tests 37-41 (the --fixup cluster) → suite is
    now full pass 50/50. Per-commit attribute resolution across multi-
    commit history is deferred; the vendored fixup fixtures are all
    single-commit so the simplification doesn't affect any test today.

  • transfer: Range-resume on interrupted downloads. The basic
    download adapter writes through <lfs_dir>/incomplete/<oid>.part
    and, when a prior attempt left a non-empty partial, sends
    Range: bytes=<offset>-<size-1> on the next attempt. Three status
    paths land:

    • 206 Partial Content → append to the partial (xfer: server accepted resume download request).
    • 416 Requested Range Not Satisfiable → delete the partial and
      recurse without Range: (xfer: server rejected resume … re-downloading from start).
    • 200 OK to a Range request → server ignored the header; treat
      as a fresh download (truncate + write).

    Partials whose size meets or exceeds the expected object size are
    treated as invalid (would produce bytes=N-(N-1)) and dropped
    before any request. GIT_CURL_VERBOSE now emits curl-style
    request/response headers on the storage GET so tests can grep
    Range:, Content-Range:, 206 Partial Content, 416 Requested Range Not Satisfiable. Mirrors upstream's tq/basic_download.go.
    Lands t-batch-storage-retries tests 3-5.

  • store: incomplete_dir() / incomplete_path(oid) /
    commit_partial(oid, path) API for the resumable-download adapter.
    Hash mismatch error message changed to expected OID {expected}, got {actual} so the upstream test suite can grep for the
    substring.

  • cli: fetch failures now emit error: failed to fetch some objects, matching upstream's commands/command_fetch.go::Exit
    format. Previously emitted one or more objects failed to download.

  • transfer: batch endpoint retries on 429 / 5xx, honoring
    Retry-After when the server pinned a wait time. Transfer::run
    now routes through a batch_with_retry helper that retries the
    batch the same way per-object transfers retry, emitting
    tq: sending batch of size N on every attempt and one
    tq: enqueue retry #N after <secs>s for "<oid>" (size: M): <err>
    per object in the batch on each retry — that's what
    t-batch-retries-ratelimit.sh greps for, since upstream's
    transfer queue routes each object through enqueueRetry at the
    batch layer. The Retry-After header now also surfaces on
    ApiError::Status via the new retry_after() accessor. Lands
    t-batch-retries-ratelimit (5 tests).

  • transfer: Retry-After header parsing on storage-action 429 / 5xx
    responses. When the server pins a wait time we sleep for exactly
    that long instead of falling back to exponential backoff, mirroring
    upstream's errors.NewRetriableLaterError gate.
    git_lfs_api::parse_retry_after is the shared helper (delta-seconds
    only today; RFC 1123 deferred until a test forces it). Lands
    t-batch-storage-retries-ratelimit (5 tests).

  • transfer: with_retry emits upstream-matching GIT_TRACE
    breadcrumbs per retry — tq: retrying object <oid> after <secs>s
    (Retry-After path) or tq: retrying object <oid>: <err> (exponential
    path), plus tq: enqueue retry #N after <secs>s for "<oid>" (size: N): <err>.
    Lands t-batch-storage-retries tests 1-2 (storage 5xx exponential
    retries).

  • transfer: action-URL error format for fatal 5xx now prefixes
    Fatal error: to match upstream's NewFatalError wrap — the
    t-batch-storage-retries greps for the exact string. 4xx and the
    non-fatal 5xx (501/507/509) keep the existing LFS: prefix that
    t-pull / t-push grep on.

  • transfer: default max_attempts bumped from 3 to 9 (= 8 retries),
    matching upstream's defaultMaxRetries = 8. Rate-limit windows
    (the test server uses 10s) outlast our previous 2-retry budget;
    the new budget covers ~25s of cumulative exponential backoff.

  • creds: NetrcCredentialHelper reads $HOME/.netrc (or _netrc
    on Windows) at construction and slots into the helper chain ahead
    of the cache. Hosts covered by netrc don't have to round-trip
    through git credential fill. Parser is permissive — recognized
    keywords are machine / default / login / password;
    unknown tokens are silently skipped so other tools' annotations
    don't break the parse. Matches upstream's
    creds/netrc.go::netrcCredentialHelper, including the trace
    format (netrc: git credential fill/approve/reject (…) with
    Go's %q quoting) the shell tests grep on.

  • api::Client: preemptive fill on subsequent requests after the
    first successful auth cycle. Re-walks the helper chain on every
    request once we've cached creds for the endpoint, so trace-emitting
    helpers (notably netrc) log a fill line per authenticated
    request — matches upstream's setRequestAuth flow under
    access=basic. Lands the two main netrc tests in t-credentials.sh
    (credentials from netrc, credentials from netrc with unknown keyword) plus one of two t-credentials-no-prompt.sh tests.

  • git/cli: http.extraHeader / http.<url>.extraHeader (multi-
    value, longest-prefix match) are now installed as default headers on
    the reqwest client backing the LFS API and transfer adapter. Same
    knob proxies and enterprise gateways use to inject Authorization or
    bookkeeping headers without going through git credential. Header
    names are case-canonicalized by reqwest (matches upstream's
    textproto.CanonicalMIMEHeaderKey), so AUTHORIZATION: and
    Authorization: map to the same header. GIT_CURL_VERBOSE echoes
    the values in the request dump so t-extra-header.sh's curl-style
    greps line up.

  • transfer: basic upload adapter now sniffs the first 512 bytes of
    each object and sets Content-Type accordingly (matches upstream's
    tq/basic_upload.go::setContentTypeFor). Sniffing covers gzip
    (1f 8b → application/x-gzip) today; broader coverage extends
    the table when a new test demands it. lfs.<url>.contenttype=false
    (with lfs.contenttype fallback) skips detection and sends
    application/octet-stream — useful when a CDN rejects sniffed
    types. On HTTP 422 from the action upload, the adapter emits
    upstream's three-line stderr nudge pointing at the disable knob.
    Lands all of t-extra-header.sh (4 tests) and t-content-type.sh
    (3 tests).

  • cli/prune: --verify-remote (-c) sends every prunable OID
    through a download-direction batch and refuses to delete anything
    the server can't serve back — protects against accidentally
    pruning the only remaining copy of a not-yet-replicated object.
    --verify-unreachable extends the check to orphan objects (those
    not reachable from any commit) too; without it, orphans pass
    through silently and are still pruned, matching upstream's
    pruneGetVerifiedPrunableObjects decision matrix.
    --when-unverified={halt|continue} controls what happens when
    some are missing — halt (default) refuses the prune and lists
    the OIDs; continue drops them from the delete set and prunes
    the rest. --no-verify-remote / --no-verify-unreachable
    override the corresponding lfs.pruneverifyremotealways /
    lfs.pruneverifyunreachablealways config keys for one
    invocation. Status line now reads X local objects, Y retained, Z verified with remote, W not on remote, done. Closes
    t-prune.sh tests 6 (prune verify) and 8 (prune unreachable)
    — t-prune.sh is now 18/18.

  • cli/fetcher: check_server_can_download(specs) companion to
    the existing check_server_has — sends a download-direction
    batch and returns the OIDs the server admits to having. Used by
    prune --verify-remote; the existing upload-direction helper
    still serves push's "skip not-on-remote" gate.

  • creds/api/cli: SSH-mediated auth via the git-lfs-authenticate
    command, the missing piece for SSH-only forge deployments. New
    creds::SshAuthClient spawns
    ssh [-p <port>] <user>@<host> git-lfs-authenticate <path> <op>,
    parses the JSON response (href, header, expires_at,
    expires_in), and caches per (host, port, path, operation) with a
    5s expiry buffer. api::SshResolver is the trait the API client
    calls before each request; a non-empty href overrides the LFS
    endpoint and header entries merge into the request. Trace lines
    (exec: <argv>, ssh cache: …, ssh cache expired: …) match
    upstream so the shell test greps line up. lfs.<url>.sshtransfer
    is partially honored: never emits the skipping pure SSH protocol trace upstream prints; always fails with git-lfs- authenticate has been disabled by request (we don't implement the
    pure-SSH transfer protocol yet). SshInfo now carries the SSH
    port so ssh -p <port> is threaded through to the command. URL
    paths returned by git-lfs-authenticate are normalized to collapse
    consecutive slashes, sidestepping a 301-redirect / POST→GET
    conversion that the reference test server (lfs-ssh-echo) would
    otherwise trip. Closes the t-batch-transfer.sh SSH test, the
    t-locks.sh SSH test (all three sub-cases), and three of six
    t-expired.sh tests (the SSH expiry trio).

  • creds: new AskpassHelper runs GIT_ASKPASS /
    core.askpass / SSH_ASKPASS (in that priority order) to prompt
    for username + password, matching upstream's
    AskPassCredentialHelper. Trace lines (creds: filling with GIT_ASKPASS: <argv>) and prompt strings (Username for "<url>",
    Password for "<scheme>://<user>@<host>") are byte-compatible with
    upstream so existing test grep patterns line up.

  • cli/fetcher: extracts user:pass@ from the LFS endpoint URL into
    an initial Auth::Basic so URL-embedded credentials skip the
    401 → fill round-trip. Builds the credential helper chain with
    askpass slotted between cache and git credential, and skips
    askpass when a credential.helper is configured (URL-prefix
    lookup matches upstream's urlConfig.Get). Inherits the git
    remote URL as the credential URL when it shares scheme+host with
    the LFS endpoint, so prompts read like Username for "https://host/repo" instead of .../repo.git/info/lfs.

  • api: Client gains with_cred_url() to override the URL used
    for credential prompts independently of the LFS endpoint;
    cred_query and CredentialsNotFound wording derive from it.
    ApiError::Status carries the request URL and renders 401/403 as
    upstream's Authorization error: <url> instead of
    server returned status …. Auth-retry now resets cached
    credentials on 403 as well as 401 so the next request fills
    fresh creds (matches upstream's per-request getCreds semantics).

Changed

  • cli/locks_verify: 401/403 from the lock-verify endpoint now
    prints upstream's full message — (error|warning): Authentication error: Authorization error: <url> — instead of the truncated
    Authentication error: lock verification failed we used before.
    Pre-push tests still match the outer Authentication error
    prefix; askpass tests pick up the inner URL-bearing
    Authorization error: <url>.

  • Credential-helper plumbing for Milestone 6:

    • creds: git credential input is now validated before each
      fill / approve / reject. Newlines and null bytes are rejected
      unconditionally; carriage returns are rejected when
      credential.protectProtocol is on (the default). Error wording
      matches upstream's creds.buffer so existing test grep patterns
      pass.
    • creds: Query::from_url now percent-decodes the path so URLs
      with %0a / %0d / %00 reach the helper as the literal byte —
      protectProtocol then catches them at the validation layer.
    • api: new ApiError::CredentialsNotFound { url, detail } variant
      surfaces upstream's Git credentials for <url> not found:\n<detail>
      wording when git credential fill returns no usable creds (or
      fails). Client honors credential.useHttpPath (default false)
      via the new with_use_http_path() builder.
    • transfer: new TransferError::BatchResponse(Box<ApiError>)
      variant prepends batch response: to API failures from the
      upload/download batch call so downstream error rendering matches
      upstream's tq wrapping. Retryability defers to the wrapped
      ApiError.
    • cli/fetcher: reads credential.useHttpPath and
      credential.protectProtocol from the effective config and
      threads both into the API client / GitCredentialHelper.
  • git lfs pointer --file=<path> now runs the configured
    lfs.extension.<name>.clean chain in priority order when invoked
    from inside a repo, producing a pointer with ext-N-<name> sha256:<input-oid> lines and emitting
    warning: Using LFS extensions, use --no-extensions for a plain pointer. on stderr. Pass --no-extensions to suppress both the
    chain and the warning. When --file is compared against
    --pointer / --stdin and the pointers don't match, prints
    note: Mismatch may be due to differing LFS extensions. if either
    side has extension lines. Closes the clean-vs-pointer-CLI
    asymmetry; smudge-side extensions already shipped.

  • git lfs ext list [<name>...] lists configured extensions, optionally
    filtered to a specific set of names. Bare git lfs ext and
    git lfs ext list (no names) keep their existing behavior of
    printing every configured extension.

  • git lfs fetch --recent (and lfs.fetchrecentalways) now expands
    the fetch set with two extras: HEAD-state of every ref whose tip
    commit lies within lfs.fetchrecentrefsdays of today, and the
    pre-image of every LFS pointer modified within
    lfs.fetchrecentcommitsdays on each anchor ref. Honors
    lfs.fetchrecentremoterefs for whether remote-tracking refs
    participate. The pre-image walk uses a new git log -G "oid sha256:" -p
    diff-parsing scanner; the recent-refs walk uses a new
    git for-each-ref --sort=-committerdate helper.

  • git lfs prune rewrites its retention model around the same
    config knobs as fetch-recent. It now retains: HEAD's tree, every
    recent ref's tree (within
    lfs.fetchrecentrefsdays + lfs.pruneoffsetdays of now), every
    recent pre-image (within
    lfs.fetchrecentcommitsdays + lfs.pruneoffsetdays of each
    anchor's tip date), and every commit reachable from any local
    branch or tag but not yet pushed (git log --branches --tags --not --remotes=<remote>). Honors lfs.fetchexclude and
    lfs.fetchinclude on the HEAD-tree, recent-ref, and pre-image
    paths; the unpushed walk runs unfiltered to match upstream.
    Adds --force (skip recent + HEAD-tree retention; keep unpushed),
    --recent (skip recent retention; keep HEAD + unpushed), and
    --no-verify-remote (no-op for now). Output strings now match
    upstream's <N> local objects, <M> retained, done. and
    Deleting objects: 100% (k/n), done. formats.

  • Prune now also retains every LFS pointer reachable from
    refs/stash (and its WIP / index / untracked merge parents),
    every staged-but-uncommitted pointer in the current worktree's
    index, and every linked worktree's HEAD-state and index.
    Mirrors upstream's pruneTaskGetRetainedStashed /
    pruneTaskGetRetainedIndex / pruneTaskGetRetainedWorktree.

  • LFS object storage and hook installation now resolve through
    git rev-parse --git-common-dir instead of --absolute-git-dir.
    In a non-worktree repo the two are identical; in a linked
    worktree the common-dir lookup returns the shared .git/
    rather than the per-worktree .git/worktrees/<name>/. This
    fixes prune from a worktree (which was looking at the wrong
    store and missing every object), git lfs install from a
    worktree (which was writing hooks to the per-worktree dir
    instead of the shared one), and the LocalGitStorageDir field
    of git lfs env. Mirrors upstream's
    Configuration.LocalGitStorageDir.

Documentation

  • Man-page sweep across every command page. Root git-lfs(1) gains
    EXAMPLES walking through the install → track → commit → push
    happy path. git-lfs-prune(1) fleshed out with DESCRIPTION /
    RECENT FILES / UNPUSHED LFS FILES / VERIFY REMOTE / DEFAULT
    REMOTE sections covering the M4 retention model. git-lfs-fetch(1)
    gains RECENT CHANGES with the lfs.fetchrecent* config keys.
    git-lfs-pull(1) gains EXAMPLES. The three migrate-*
    subcommand pages get per-mode EXAMPLES + SEE ALSO cross-
    references. git-lfs-smudge(1) gets SEE ALSO; git-lfs-ext(1)
    gets EXAMPLES. Section order across fetch / pull /
    migrate now matches upstream's INCLUDE AND EXCLUDE → DEFAULT
    REMOTE → DEFAULT REFS → RECENT CHANGES → EXAMPLES → SEE ALSO
    flow.
  • git-lfs-config(5) rewritten from a 21-line stub to a 219-line
    reference: CONFIGURATION FILES (precedence + .lfsconfig
    lookup chain + lfs.<url>.<key> overrides), GENERAL /
    UPLOAD AND DOWNLOAD TRANSFER / PUSH / FETCH / PRUNE / EXTENSIONS
    / OTHER subsections covering ~27 config keys we honor, LFSCONFIG
    with the allowed-key list, EXAMPLES, and SEE ALSO. Keys we
    don't implement (custom-transfer agents, NTLM, dial/tls
    timeouts, etc.) are silently omitted.
  • Installation instructions now cover all three packaging paths:
    Homebrew tap (Linux + macOS), Debian/Ubuntu apt, Fedora/RHEL
    dnf, plus the existing cargo install. README has the
    copy-paste-ready commands inline; docs/install.md adds context
    including the git-lfs-rs package-name caveat and the post-
    install git lfs install step.
  • Two latent bugs in the groff converter fixed in passing: ordered
    lists rendered with bullet markers (ListKind::Ordered collapsed
    the start number — now emits .IP "N." 4 and increments per item),
    and list-item-first-paragraph leaked across tight-list boundaries
    (suppress_next_paragraph flag now cleared on End(Item)).
  • cli/man/<cmd>/ directory grew supporting markdown for the
    above. Per-subcommand ManContent entries in cli/src/man.rs
    wire them into both the man-page and mdbook output.