Release v0.6.0
Fixed
api:ApiError::StatusDisplay now surfaces the server's body
messageverbatim 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-credentialsall
grep for. Lands 5 tests (3 near-misses across 3 suites, plus 2
bonus from suites that shared the same root cause).creds:HelperChain::fillnow skips helpers that error and
continues to the next, matching upstream'sCredentialHelpers.Fill
(creds/creds.go:502). Previously a failed askpass program
short-circuited the chain beforegit credentialgot a turn,
so a missingGIT_ASKPASSwould lock the user out instead of
falling through to the configured credential helper.creds: emitcreds: failed to find GIT_ASKPASS command: <prog>
when the askpass executable isn't onPATH, and
creds: git credential <sub> (<proto>, <host>, <path>)on every
git credentialinvocation. Both match upstream'stracerx.Printf
format atcreds/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--allor
--recent. Matches upstream'sfetchRefvsfetchRefssplit
and is a prerequisite for the upcoming--recentsemantics.
Added
-
migrate info --fixupnow does the real per-tree attribute walk:
list every blob at the selected ref, build a freshAttrSetfrom
that tree's.gitattributesfiles (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::BlobFnfixup branch. Lands
t-migrate-infotests 37-41 (the--fixupcluster) → 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 withoutRange:(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 producebytes=N-(N-1)) and dropped
before any request.GIT_CURL_VERBOSEnow 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'stq/basic_download.go.
Landst-batch-storage-retriestests 3-5. - 206 Partial Content → append to the partial (
-
store:incomplete_dir()/incomplete_path(oid)/
commit_partial(oid, path)API for the resumable-download adapter.
Hash mismatch error message changed toexpected OID {expected}, got {actual}so the upstream test suite can grep for the
substring. -
cli: fetch failures now emiterror: failed to fetch some objects, matching upstream'scommands/command_fetch.go::Exit
format. Previously emittedone or more objects failed to download. -
transfer: batch endpoint retries on 429 / 5xx, honoring
Retry-Afterwhen the server pinned a wait time.Transfer::run
now routes through abatch_with_retryhelper that retries the
batch the same way per-object transfers retry, emitting
tq: sending batch of size Non 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.shgreps for, since upstream's
transfer queue routes each object throughenqueueRetryat the
batch layer. TheRetry-Afterheader now also surfaces on
ApiError::Statusvia the newretry_after()accessor. Lands
t-batch-retries-ratelimit(5 tests). -
transfer:Retry-Afterheader 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'serrors.NewRetriableLaterErrorgate.
git_lfs_api::parse_retry_afteris 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_retryemits upstream-matching GIT_TRACE
breadcrumbs per retry —tq: retrying object <oid> after <secs>s
(Retry-After path) ortq: retrying object <oid>: <err>(exponential
path), plustq: enqueue retry #N after <secs>s for "<oid>" (size: N): <err>.
Landst-batch-storage-retriestests 1-2 (storage 5xx exponential
retries). -
transfer: action-URL error format for fatal 5xx now prefixes
Fatal error:to match upstream'sNewFatalErrorwrap — the
t-batch-storage-retriesgreps for the exact string. 4xx and the
non-fatal 5xx (501/507/509) keep the existingLFS:prefix that
t-pull/t-pushgrep on. -
transfer: defaultmax_attemptsbumped from 3 to 9 (= 8 retries),
matching upstream'sdefaultMaxRetries = 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:NetrcCredentialHelperreads$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
throughgit credential fill. Parser is permissive — recognized
keywords aremachine/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%qquoting) 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 afillline per authenticated
request — matches upstream'ssetRequestAuthflow under
access=basic. Lands the two main netrc tests int-credentials.sh
(credentials from netrc,credentials from netrc with unknown keyword) plus one of twot-credentials-no-prompt.shtests. -
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 throughgit credential. Header
names are case-canonicalized by reqwest (matches upstream's
textproto.CanonicalMIMEHeaderKey), soAUTHORIZATION:and
Authorization:map to the same header.GIT_CURL_VERBOSEechoes
the values in the request dump sot-extra-header.sh's curl-style
greps line up. -
transfer: basic upload adapter now sniffs the first 512 bytes of
each object and setsContent-Typeaccordingly (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
(withlfs.contenttypefallback) 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 oft-extra-header.sh(4 tests) andt-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-unreachableextends 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
pruneGetVerifiedPrunableObjectsdecision matrix.
--when-unverified={halt|continue}controls what happens when
some are missing —halt(default) refuses the prune and lists
the OIDs;continuedrops them from the delete set and prunes
the rest.--no-verify-remote/--no-verify-unreachable
override the correspondinglfs.pruneverifyremotealways/
lfs.pruneverifyunreachablealwaysconfig keys for one
invocation. Status line now readsX local objects, Y retained, Z verified with remote, W not on remote, done.Closes
t-prune.shtests 6 (prune verify) and 8 (prune unreachable)
—t-prune.shis now 18/18. -
cli/fetcher:check_server_can_download(specs)companion to
the existingcheck_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 thegit-lfs-authenticate
command, the missing piece for SSH-only forge deployments. New
creds::SshAuthClientspawns
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::SshResolveris the trait the API client
calls before each request; a non-emptyhrefoverrides the LFS
endpoint andheaderentries 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:neveremits theskipping pure SSH protocoltrace upstream prints;alwaysfails withgit-lfs- authenticate has been disabled by request(we don't implement the
pure-SSH transfer protocol yet).SshInfonow carries the SSH
port sossh -p <port>is threaded through to the command. URL
paths returned bygit-lfs-authenticateare normalized to collapse
consecutive slashes, sidestepping a 301-redirect / POST→GET
conversion that the reference test server (lfs-ssh-echo) would
otherwise trip. Closes thet-batch-transfer.shSSH test, the
t-locks.shSSH test (all three sub-cases), and three of six
t-expired.shtests (the SSH expiry trio). -
creds: newAskpassHelperrunsGIT_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: extractsuser:pass@from the LFS endpoint URL into
an initialAuth::Basicso URL-embedded credentials skip the
401 → fill round-trip. Builds the credential helper chain with
askpass slotted between cache andgit credential, and skips
askpass when acredential.helperis configured (URL-prefix
lookup matches upstream'surlConfig.Get). Inherits the git
remote URL as the credential URL when it shares scheme+host with
the LFS endpoint, so prompts read likeUsername for "https://host/repo"instead of.../repo.git/info/lfs. -
api:Clientgainswith_cred_url()to override the URL used
for credential prompts independently of the LFS endpoint;
cred_queryandCredentialsNotFoundwording derive from it.
ApiError::Statuscarries the request URL and renders 401/403 as
upstream'sAuthorization 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-requestgetCredssemantics).
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 failedwe used before.
Pre-push tests still match the outerAuthentication error
prefix; askpass tests pick up the inner URL-bearing
Authorization error: <url>. -
Credential-helper plumbing for Milestone 6:
creds:git credentialinput is now validated before each
fill/approve/reject. Newlines and null bytes are rejected
unconditionally; carriage returns are rejected when
credential.protectProtocolis on (the default). Error wording
matches upstream'screds.bufferso existing test grep patterns
pass.creds:Query::from_urlnow percent-decodes the path so URLs
with%0a/%0d/%00reach the helper as the literal byte —
protectProtocolthen catches them at the validation layer.api: newApiError::CredentialsNotFound { url, detail }variant
surfaces upstream'sGit credentials for <url> not found:\n<detail>
wording whengit credential fillreturns no usable creds (or
fails).Clienthonorscredential.useHttpPath(defaultfalse)
via the newwith_use_http_path()builder.transfer: newTransferError::BatchResponse(Box<ApiError>)
variant prependsbatch response:to API failures from the
upload/download batch call so downstream error rendering matches
upstream'stqwrapping. Retryability defers to the wrapped
ApiError.cli/fetcher: readscredential.useHttpPathand
credential.protectProtocolfrom the effective config and
threads both into the API client /GitCredentialHelper.
-
git lfs pointer --file=<path>now runs the configured
lfs.extension.<name>.cleanchain in priority order when invoked
from inside a repo, producing a pointer withext-N-<name> sha256:<input-oid>lines and emitting
warning: Using LFS extensions, use --no-extensions for a plain pointer.on stderr. Pass--no-extensionsto suppress both the
chain and the warning. When--fileis compared against
--pointer/--stdinand 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. Baregit lfs extand
git lfs ext list(no names) keep their existing behavior of
printing every configured extension. -
git lfs fetch --recent(andlfs.fetchrecentalways) now expands
the fetch set with two extras: HEAD-state of every ref whose tip
commit lies withinlfs.fetchrecentrefsdaysof today, and the
pre-image of every LFS pointer modified within
lfs.fetchrecentcommitsdayson each anchor ref. Honors
lfs.fetchrecentremoterefsfor whether remote-tracking refs
participate. The pre-image walk uses a newgit log -G "oid sha256:" -p
diff-parsing scanner; the recent-refs walk uses a new
git for-each-ref --sort=-committerdatehelper. -
git lfs prunerewrites 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.pruneoffsetdaysof now), every
recent pre-image (within
lfs.fetchrecentcommitsdays + lfs.pruneoffsetdaysof 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>). Honorslfs.fetchexcludeand
lfs.fetchincludeon 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'spruneTaskGetRetainedStashed/
pruneTaskGetRetainedIndex/pruneTaskGetRetainedWorktree. -
LFS object storage and hook installation now resolve through
git rev-parse --git-common-dirinstead 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 installfrom a
worktree (which was writing hooks to the per-worktree dir
instead of the shared one), and theLocalGitStorageDirfield
ofgit 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 thelfs.fetchrecent*config keys.
git-lfs-pull(1)gains EXAMPLES. The threemigrate-*
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 acrossfetch/pull/
migratenow 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 existingcargo install. README has the
copy-paste-ready commands inline;docs/install.mdadds context
including thegit-lfs-rspackage-name caveat and the post-
installgit lfs installstep. - Two latent bugs in the groff converter fixed in passing: ordered
lists rendered with bullet markers (ListKind::Orderedcollapsed
the start number — now emits.IP "N." 4and increments per item),
and list-item-first-paragraph leaked across tight-list boundaries
(suppress_next_paragraphflag now cleared onEnd(Item)). cli/man/<cmd>/directory grew supporting markdown for the
above. Per-subcommandManContententries incli/src/man.rs
wire them into both the man-page and mdbook output.