Pack requests stage downloads in a predictable partial-pack file so an
interrupted transfer can be resumed. Both packfile URI and ordinary dumb
HTTP requests use this staging path. Opening it in append mode forces
each write to the current end of the file, so concurrent responses can
append duplicate data and corrupt the pack.
Open the partial pack read-write without O_APPEND and seek once to its
current end. Each downloader then retains the offset matching the Range
it requested. Because the staging key must uniquely identify immutable
pack contents, overlapping responses write the same bytes at the same
offsets instead of extending the file with duplicate data.
MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an
existing file. Create a missing partial pack exclusively, close it, and
reopen it without O_CREAT so every retained descriptor permits another
downloader to unlink the staging path. Duplicate that descriptor for
index-pack instead of reopening the path after closing the stream;
index-pack installs its own pack and the shared staging file is only
unlinked, never renamed. Accept HTTP 416 when a partial pack is already
complete and let index-pack validate its contents.
Exercise resumed transfers, EOF ranges, overlapping 200 and 206
responses, and unlinking the staging path while index-pack still holds
its descriptor. Clarify the staging-key documentation.
Signed-off-by: Ted Nyman <tnyman@openai.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Ted Nyman committedJul 21, 2026 at 16:29 UTC0125ac2f19d95a9b441a1ef5dc13d1059fbb0362
6 files changed+289-25
Documentation/git-http-fetch.adoc
+3-2
index 09b5d675ee..60ca91cf3a 100644--- a/Documentation/git-http-fetch.adoc+++ b/Documentation/git-http-fetch.adoc@@ -48,8 +48,9 @@ commit-id:: line (which is not expected in this case), 'git http-fetch' fetches the packfile directly at the given URL and uses index-pack to generate corresponding .idx and .keep files.- The hash is used to determine the name of the temporary file and is- arbitrary. The output of index-pack is printed to stdout. Requires+ The hash is used to determine the name of the temporary file. It need+ not be the pack hash, but it must uniquely identify the pack contents+ for resumption. The output of index-pack is printed to stdout. Requires one or more --index-pack-arg options. --index-pack-arg=<arg>::
http-fetch.c
+2-1
index 601a77c3c1..05f68f306a 100644--- a/http-fetch.c+++ b/http-fetch.c@@ -70,7 +70,8 @@ static void fetch_single_packfile(struct object_id *packfile_hash, if (start_active_slot(preq->slot)) { run_active_slot(preq->slot);- if (results.curl_result != CURLE_OK) {+ if (results.curl_result != CURLE_OK &&+ results.http_code != 416) { struct url_info url; char *nurl = url_normalize(preq->url, &url); if (!nurl || !git_env_bool("GIT_TRACE_REDACT", 1)) {
http-push.c
+2-1
index 3c23cbba27..03dc8102a1 100644--- a/http-push.c+++ b/http-push.c@@ -595,7 +595,8 @@ static void finish_request(struct transfer_request *request) } else if (request->state == RUN_FETCH_PACKED) { int fail = 1;- if (request->curl_result != CURLE_OK) {+ if (request->curl_result != CURLE_OK &&+ request->http_code != 416) { fprintf(stderr, "Unable to get pack file %s\n%s", request->url, curl_errorstr); } else {
http-walker.c
+2-1
index b58a3b2a92..abafca84d6 100644--- a/http-walker.c+++ b/http-walker.c@@ -451,7 +451,8 @@ static int http_fetch_pack(struct walker *walker, struct alt_base *repo, if (start_active_slot(preq->slot)) { run_active_slot(preq->slot);- if (results.curl_result != CURLE_OK) {+ if (results.curl_result != CURLE_OK &&+ results.http_code != 416) { error("Unable to get pack file %s\n%s", preq->url, curl_errorstr); goto abort;
http.c
+36-20
index b4e7b8d00b..a530995363 100644--- a/http.c+++ b/http.c@@ -2688,10 +2688,13 @@ int finish_http_pack_request(struct http_pack_request *preq) int tmpfile_fd; int ret = 0;+ /* Another downloader may unlink the staging path while we index it. */+ tmpfile_fd = xdup(fileno(preq->packfile)); fclose(preq->packfile); preq->packfile = NULL;-- tmpfile_fd = xopen(preq->tmpfile.buf, O_RDONLY);+ if (lseek(tmpfile_fd, 0, SEEK_SET) < 0)+ die_errno("unable to seek local file %s for pack",+ preq->tmpfile.buf); ip.git_cmd = 1; ip.in = tmpfile_fd;@@ -2704,13 +2707,8 @@ int finish_http_pack_request(struct http_pack_request *preq) else ip.no_stdout = 1;- if (run_command(&ip)) {+ if (run_command(&ip)) ret = -1;- goto cleanup;- }--cleanup:- close(tmpfile_fd); unlink(preq->tmpfile.buf); return ret; }@@ -2738,22 +2736,45 @@ struct http_pack_request *new_http_pack_request( struct http_pack_request *new_direct_http_pack_request( const unsigned char *packed_git_hash, char *url) {- off_t prev_posn = 0;+ off_t prev_posn; struct http_pack_request *preq;+ int fd; CALLOC_ARRAY(preq, 1); strbuf_init(&preq->tmpfile, 0);- preq->url = url; odb_pack_name(the_repository, &preq->tmpfile, packed_git_hash, "pack"); strbuf_addstr(&preq->tmpfile, ".temp");- preq->packfile = fopen(preq->tmpfile.buf, "a");- if (!preq->packfile) {- error("Unable to open local file %s for pack",- preq->tmpfile.buf);+ /*+ * MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an+ * existing file; reopen a newly created file so others may unlink it.+ */+ for (;;) {+ fd = open(preq->tmpfile.buf, O_RDWR);+ if (fd >= 0 || errno != ENOENT)+ break;+ fd = open(preq->tmpfile.buf, O_RDWR | O_CREAT | O_EXCL, 0666);+ if (fd >= 0) {+ close(fd);+ continue;+ }+ if (errno != EEXIST)+ break;+ }+ if (fd < 0) {+ error_errno("unable to open local file %s for pack",+ preq->tmpfile.buf); goto abort; }+ prev_posn = lseek(fd, 0, SEEK_END);+ if (prev_posn < 0) {+ error_errno("unable to seek local file %s for pack",+ preq->tmpfile.buf);+ close(fd);+ goto abort;+ }+ preq->packfile = xfdopen(fd, "w"); preq->slot = get_active_slot(); preq->headers = object_request_headers();@@ -2762,12 +2783,7 @@ struct http_pack_request *new_direct_http_pack_request( curl_easy_setopt(preq->slot->curl, CURLOPT_URL, preq->url); curl_easy_setopt(preq->slot->curl, CURLOPT_HTTPHEADER, preq->headers);- /*- * If there is data present from a previous transfer attempt,- * resume where it left off- */- prev_posn = ftello(preq->packfile);- if (prev_posn>0) {+ if (prev_posn > 0) { if (http_is_verbose) fprintf(stderr, "Resuming fetch of pack %s at byte %"PRIuMAX"\n",