[PATCH v5 0/3] packfile URIs: support concurrent downloads
- From
Ted Nyman <tnyman@openai.com>
- Date
- Jul 26, 2026, 06:44 UTC
- Message-ID
- <cover.1785047139.git.tnyman@openai.com>
- In-Reply-To
- <cover.1784874850.git.tnyman@openai.com>
Packfile URI and dumb HTTP downloads stage packs at objects/pack/pack-<hash>.pack.temp so an interrupted transfer can resume. Opening that file in append mode forces every write to its current end. Two Git processes fetching the same pack into one object database can therefore append duplicate data and corrupt the pack.
The first patch separates the unrelated --index-pack-arg documentation and error-message correction requested during review.
The second patch keeps the predictable staging name but removes append mode. Each downloader seeks once to the current end, requests the corresponding Range, and writes using its own descriptor offset. Since the staging key must identify immutable pack contents, overlapping responses write identical bytes at identical offsets. There is no need for pwrite(2) or cross-process coordination, and resumption continues to work for both packfile URI and ordinary dumb HTTP downloads.
A downloader can also find that the partial pack has completed and request a range starting at EOF. Servers may respond with HTTP 416 in that case. Treat the response as a completed download and let index-pack validate the pack.
On MinGW, the non-append O_RDWR open grants FILE_SHARE_DELETE only for an existing file. Create a missing staging file exclusively, close it, and reopen it without O_CREAT so every retained descriptor permits another downloader to unlink the path. Keep the open descriptor for index-pack; it installs its own pack, so the shared staging file is only unlinked, never renamed.
The third patch handles the related .keep race. When another process has already created the keep file, index-pack reports "pack<TAB><hash>" instead of "keep<TAB><hash>". Accept both successful forms and remove only keep files created by the current process. Read only the prefix and hash so any following fsck output remains available to fetch-pack.
The tests cover resumption, a completed partial returning 416, overlapping downloads, unlinking the staging path while index-pack holds its descriptor, and a pre-existing .keep file. The unlink test does not require FIFOs, so it can exercise MinGW's sharing behavior even though the concurrent-download tests are skipped there.
Changes since v4:
* Clarify that the first --index-pack-arg specifies the command and
subsequent instances specify its arguments.
* Drop assumptions about which concurrent response reaches the
staging file first. Either write order exercises the same
overlapping-download behavior.
* No production code changes.The overlapping-download test passes 240 runs with 12 parallel stress jobs.
The v4 discussion is at:
https://lore.kernel.org/git/cover.1784874850.git.tnyman@openai.com/
Ted Nyman (3): http-fetch: correct --index-pack-arg documentation http: avoid concurrent appends to partial packs fetch-pack: accept "pack" output for packfile URIs
Documentation/git-http-fetch.adoc | 14 +- fetch-pack.c | 33 ++-- http-fetch.c | 7 +- http-push.c | 3 +- http-walker.c | 3 +- http.c | 56 ++++--- t/t5550-http-fetch-dumb.sh | 246 ++++++++++++++++++++++++++++++ t/t5702-protocol-v2.sh | 31 ++++ 8 files changed, 347 insertions(+), 46 deletions(-)
Range-diff against v4:
1: a6a40b8046 ! 1: a79af009ea http-fetch: correct --index-pack-arg documentation
@@ Documentation/git-http-fetch.adoc: commit-id::
- For internal use only. The command to run on the contents of the
- downloaded pack. Arguments are URL-encoded separated by spaces.
+--index-pack-arg=<arg>::
-+ For internal use only. An argument to the command run on the contents
-+ of the downloaded pack. This option can be specified multiple times.
++ For internal use only. The first instance specifies the command run on
++ the contents of the downloaded pack. Subsequent instances specify its
++ arguments.
--recover::
Verify that everything reachable from target is fetched. Used after
2: 144c98cdfa ! 2: d9667c93b0 http: avoid concurrent appends to partial packs
@@ t/t5550-http-fetch-dumb.sh: test_expect_success 'http-fetch --packfile' '
+ read ready <&8 &&
+ test "$ready" = ready &&
+ test_path_is_file "$tmpfile" &&
-+ test -s "$tmpfile" &&
+ {
+ GIT_TRACE_CURL="$TRASH_DIRECTORY/overlap-second.trace" \
+ GIT_TRACE_CURL_NO_DATA=1 \
@@ t/t5550-http-fetch-dumb.sh: test_expect_success 'http-fetch --packfile' '
+ wait "$second_pid" &&
+ wait "$first_pid" &&
+ wait "$server_pid" &&
-+ test_grep "HTTP/[0-9.]* 200" overlap-first.trace &&
-+ test_grep "Range: bytes=[1-9][0-9]*-" overlap-second.trace &&
-+ test_grep "HTTP/[0-9.]* 206" overlap-second.trace &&
+ printf "keep\t%s\npack\t%s\n" "$packhash" "$packhash" | sort >expect &&
+ sort first.out second.out >actual &&
+ test_cmp expect actual &&
3: d9063deb60 = 3: fee6f292cb fetch-pack: accept "pack" output for packfile URIsbase-commit: 5d2e7709234afea1b6ddb25cd4f60d3d5fb3c200
-- 2.55.0.openai.131.g83a728de1eb6