http: permit unlinking partial packs on Windows
On Windows, an open file must permit FILE_SHARE_DELETE before another process can unlink it. MinGW's non-append O_RDWR open enables that sharing mode only for an existing file; adding O_CREAT falls back to _wopen(), which cannot set it. First try opening the partial pack without O_CREAT. If it does not exist, create it exclusively, close that descriptor, and retry through the existing-file path. A racing creator retries after EEXIST. This ensures that every retained descriptor permits another downloader to unlink the staging path. Add an unlink-while-indexing test that does not require FIFOs and can therefore run on MinGW. Signed-off-by: Ted Nyman <tnyman@openai.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Ted Nyman committed
Jul 26, 2026 at 17:28 UTC
e92a518a741ead85634c43f871e7e095bcf66f1f
2 files changed
+37
-1
http.c
+16
-1
@@ -2746,7 +2746,22 @@ struct http_pack_request *new_direct_http_pack_request(
2746
2747
odb_pack_name(the_repository, &preq->tmpfile, packed_git_hash, "pack");
2748
strbuf_addstr(&preq->tmpfile, ".temp");
2749
- fd = open(preq->tmpfile.buf, O_RDWR | O_CREAT, 0666);
2749
+ /*
2750
+ * MinGW's non-append O_RDWR open grants FILE_SHARE_DELETE only for an
2751
+ * existing file; reopen a newly created file so others may unlink it.
2752
+ */
2753
+ for (;;) {
2754
+ fd = open(preq->tmpfile.buf, O_RDWR);
2755
+ if (fd >= 0 || errno != ENOENT)
2756
+ break;
2757
+ fd = open(preq->tmpfile.buf, O_RDWR | O_CREAT | O_EXCL, 0666);
2758
+ if (fd >= 0) {
2759
+ close(fd);
2760
+ continue;
2761
+ }
2762
+ if (errno != EEXIST)
2763
+ break;
2764
+ }
2765
if (fd < 0) {
2766
error_errno("unable to open local file %s for pack",
2767
preq->tmpfile.buf);
t/t5550-http-fetch-dumb.sh
+21
@@ -328,6 +328,27 @@ test_expect_success 'http-fetch --packfile resumes a partial download' '
328
git -C packfileclient-resume cat-file -e "$HASH"
329
'
330
331
+test_expect_success 'http-fetch --packfile permits unlink while indexing' '
332
+ git init packfileclient-unlink &&
333
+ p=$(cd "$HTTPD_DOCUMENT_ROOT_PATH"/repo_pack.git &&
334
+ ls objects/pack/pack-*.pack) &&
335
+ tmpfile="packfileclient-unlink/.git/objects/pack/pack-$ARBITRARY.pack.temp" &&
336
+ write_script git-unlink-index-pack <<-\EOF &&
337
+ test -f "$GIT_TEST_PACK_TEMP" || exit 1
338
+ rm "$GIT_TEST_PACK_TEMP" || exit 1
339
+ exec git index-pack "$@"
340
+ EOF
341
+ test_when_finished "rm -f git-unlink-index-pack" &&
342
+ PATH="$TRASH_DIRECTORY:$PATH" \
343
+ GIT_TEST_PACK_TEMP="$TRASH_DIRECTORY/$tmpfile" \
344
+ git -C packfileclient-unlink http-fetch --packfile="$ARBITRARY" \
345
+ --index-pack-arg=unlink-index-pack \
346
+ --index-pack-arg=--stdin --index-pack-arg=--keep \
347
+ "$HTTPD_URL/dumb/repo_pack.git/$p" >out &&
348
+ test_path_is_missing "$tmpfile" &&
349
+ git -C packfileclient-unlink cat-file -e "$HASH"
350
+'
351
+
352
test_expect_success PERL,PIPE 'concurrent http-fetch --packfile cannot corrupt an overlapping download' '
353
git init packfileclient-overlap &&
354
blob=$(test-tool genrandom pack-overlap 2m |