diff --git a/object-file.c b/object-file.c index c049bf13f7..132aaab6f8 100644 --- a/object-file.c +++ b/object-file.c @@ -491,6 +491,7 @@ struct odb_transaction_files { struct transaction_packfile packfile; const char *prefix; + struct strvec pack_names; struct tempfile **pack_lockfiles; size_t pack_lockfiles_nr; size_t pack_lockfiles_alloc; @@ -1253,6 +1254,82 @@ out: return ret; } +static int write_pack_lockfile(struct odb_source *source, const char *pack_name, + const char *keep_msg, struct tempfile **out) +{ + struct strbuf lock_path = STRBUF_INIT; + struct strbuf contents = STRBUF_INIT; + int ret; + int fd; + + strbuf_addf(&lock_path, "%s/pack/pack-%s.keep", + source->path, pack_name); + strbuf_addf(&contents, "%s\n", keep_msg); + + fd = safe_create_file_with_leading_directories(source->odb->repo, + lock_path.buf); + if (fd < 0) { + ret = error_errno(_("unable to create '%s'"), lock_path.buf); + goto out; + } + + if (write_in_full(fd, contents.buf, contents.len) < 0) { + ret = error_errno(_("unable to write '%s'"), lock_path.buf); + close(fd); + unlink(lock_path.buf); + goto out; + } + + if (close(fd) < 0) { + ret = error_errno(_("unable to close '%s'"), lock_path.buf); + unlink(lock_path.buf); + goto out; + } + + *out = register_tempfile(lock_path.buf); + ret = 0; + +out: + strbuf_release(&lock_path); + strbuf_release(&contents); + + return ret; +} + +static int write_pack_lockfiles(struct odb_transaction_files *transaction) +{ + struct strbuf keep_msg = STRBUF_INIT; + char hostname[HOST_NAME_MAX + 1]; + int ret = 0; + + if (!transaction->pack_names.nr) + return 0; + + if (xgethostname(hostname, sizeof(hostname))) + xsnprintf(hostname, sizeof(hostname), "localhost"); + strbuf_addf(&keep_msg, "receive-pack %"PRIuMAX" on %s", + (uintmax_t)getpid(), hostname); + + for (size_t i = 0; i < transaction->pack_names.nr; i++) { + struct tempfile *lock; + + ret = write_pack_lockfile(transaction->base.source, + transaction->pack_names.v[i], + keep_msg.buf, &lock); + if (ret < 0) + break; + + ALLOC_GROW(transaction->pack_lockfiles, + transaction->pack_lockfiles_nr + 1, + transaction->pack_lockfiles_alloc); + transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] = lock; + } + + strbuf_release(&keep_msg); + + return ret; +} + static int odb_transaction_files_commit(struct odb_transaction *base) { struct odb_transaction_files *transaction = @@ -1281,6 +1358,9 @@ static int odb_transaction_files_commit(struct odb_transaction *base) delete_tempfile(&temp); strbuf_release(&temp_path); + if (write_pack_lockfiles(transaction)) + return error(_("unable to lock packfiles")); + /* * Make the object files visible in the primary ODB after their data is * fully durable. @@ -1345,9 +1425,11 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base, container_of(base, struct odb_transaction_files, base); struct repository *repo = base->source->odb->repo; struct child_process child = CHILD_PROCESS_INIT; + struct strbuf pack_name = STRBUF_INIT; struct pack_header hdr; const char *hdr_err; int err_fd = opts->err_fd; + int ret = -1; int status; hdr_err = parse_pack_header(&hdr, pack_fd); @@ -1386,19 +1468,9 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base, return -1; } } else { - char hostname[HOST_NAME_MAX + 1]; - char *lockfile; - strvec_pushl(&child.args, "index-pack", "--stdin", NULL); push_header_arg(&child.args, &hdr); - if (xgethostname(hostname, sizeof(hostname))) - xsnprintf(hostname, sizeof(hostname), "localhost"); - strvec_pushf(&child.args, - "--keep=receive-pack %"PRIuMAX" on %s", - (uintmax_t)getpid(), - hostname); - if (!opts->quiet && err_fd) strvec_push(&child.args, "--show-resolving-progress"); if (err_fd) @@ -1421,33 +1493,32 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base, return -1; } - /* - * The lockfile filepath is expected to be the final location of - * the ".keep" file after being migrated to the main ODB source. - * This ensures the lockfile can be found and removed later - * after the ODB transaction has been committed. - */ - lockfile = index_pack_lockfile(base->source, child.out, NULL); - if (lockfile) { - ALLOC_GROW(transaction->pack_lockfiles, - transaction->pack_lockfiles_nr + 1, - transaction->pack_lockfiles_alloc); - transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] = - register_tempfile(lockfile); - free(lockfile); - } + ret = read_index_pack_report(repo, child.out, &pack_name, NULL); + close(child.out); status = finish_command(&child); if (status) { strbuf_addstr(err_msg, "index-pack abnormal exit"); - return -1; + goto out; } + if (ret) { + strbuf_addstr(err_msg, "index-pack failed to report pack name"); + goto out; + } + + strvec_push(&transaction->pack_names, pack_name.buf); + odb_source_prepare(base->source, ODB_PREPARE_FLUSH_CACHES); } - return 0; + ret = 0; + +out: + strbuf_release(&pack_name); + + return ret; } static int odb_transaction_files_finalize(struct odb_transaction *base) @@ -1460,6 +1531,7 @@ static int odb_transaction_files_finalize(struct odb_transaction *base) ret |= delete_tempfile(&transaction->pack_lockfiles[i]); free(transaction->pack_lockfiles); + strvec_clear(&transaction->pack_names); return ret; } @@ -1492,6 +1564,7 @@ int odb_transaction_files_begin(struct odb_source *source, transaction->base.write_pack = odb_transaction_files_write_pack; transaction->base.env = odb_transaction_files_env; transaction->flags = flags; + strvec_init(&transaction->pack_names); transaction->prefix = "bulk-fsync"; if (flags & ODB_TRANSACTION_RECEIVE) { diff --git a/pack-write.c b/pack-write.c index 85674e4b72..2ed33448ba 100644 --- a/pack-write.c +++ b/pack-write.c @@ -469,33 +469,63 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo, fsync_component_or_die(FSYNC_COMPONENT_PACK, pack_fd, pack_name); } +int read_index_pack_report(struct repository *repo, int ip_out, + struct strbuf *pack_name, int *is_keep) +{ + const int len = repo->hash_algo->hexsz + 6; + char packname[GIT_MAX_HEXSZ + 6]; + int is_keep_ignored; + const char *name; + + if (!is_keep) + is_keep = &is_keep_ignored; + + /* + * The first thing we expect from index-pack's output is + * "pack\t\n" or "keep\t\n", where is the hexadecimal + * name of the packfile it has just written. The line is therefore five + * bytes of prefix, `hexsz` bytes of name, and a trailing newline. A + * "keep" prefix tells us that index-pack has also created the + * corresponding ".keep" file. + */ + if (read_in_full(ip_out, packname, len) != len || packname[len - 1] != '\n') + return -1; + + packname[len - 1] = 0; + if (skip_prefix(packname, "pack\t", &name)) + *is_keep = 0; + else if (skip_prefix(packname, "keep\t", &name)) + *is_keep = 1; + else + return -1; + + strbuf_addstr(pack_name, name); + + return 0; +} + char *index_pack_lockfile(struct odb_source *source, int ip_out, int *is_well_formed) { - char packname[GIT_MAX_HEXSZ + 6]; - const int len = source->odb->repo->hash_algo->hexsz + 6; - - /* - * The first thing we expect from index-pack's output - * is "pack\t%40s\n" or "keep\t%40s\n" (46 bytes) where - * %40s is the newly created pack SHA1 name. In the "keep" - * case, we need it to remove the corresponding .keep file - * later on. If we don't get that then tough luck with it. - */ - if (read_in_full(ip_out, packname, len) == len && packname[len-1] == '\n') { - const char *name; + struct strbuf pack_name = STRBUF_INIT; + char *lockfile = NULL; + int is_keep = 0; + if (read_index_pack_report(source->odb->repo, ip_out, &pack_name, &is_keep)) { if (is_well_formed) - *is_well_formed = 1; - packname[len-1] = 0; - if (skip_prefix(packname, "keep\t", &name)) - return xstrfmt("%s/pack/pack-%s.keep", - source->path, name); + *is_well_formed = 0; return NULL; } + if (is_well_formed) - *is_well_formed = 0; - return NULL; + *is_well_formed = 1; + + /* If there is no ".keep" file, then there is no lockfile to return. */ + if (is_keep) + lockfile = xstrfmt("%s/pack/pack-%s.keep", source->path, pack_name.buf); + + strbuf_release(&pack_name); + return lockfile; } /* diff --git a/pack.h b/pack.h index ada506b5c5..2f2ce97c11 100644 --- a/pack.h +++ b/pack.h @@ -106,6 +106,16 @@ off_t write_pack_header(struct hashfile *f, uint32_t); void fixup_pack_header_footer(const struct git_hash_algo *, int, unsigned char *, const char *, uint32_t, unsigned char *, off_t); + +/* + * Read the report that git-index-pack(1) writes to its standard output after + * the packfile is written. The hexadecimal name of the packfile is appended to + * `pack_name` and, if `is_keep` is non-NULL, it is set to indicate whether a + * ".keep" file was written. Return 0 on success and a negative error code on + * failure. + */ +int read_index_pack_report(struct repository *repo, int ip_out, + struct strbuf *pack_name, int *is_keep); char *index_pack_lockfile(struct odb_source *source, int fd, int *is_well_formed); diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh index 1b7097179e..6c14dfc42b 100755 --- a/t/t5547-push-quarantine.sh +++ b/t/t5547-push-quarantine.sh @@ -77,10 +77,9 @@ test_expect_success '.keep file is removed after push' ' git -C keep.git config set receive.unpackLimit 0 && # While incoming objects are still quarantined, validate that the - # ".keep" lockfile is present in the quarantine directory. + # ".keep" lockfile is not present in the quarantine directory. test_hook -C keep.git pre-receive <<-\EOF && - keep="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.keep)" && - test -f "$keep" + test ! -e "$GIT_QUARANTINE_PATH"/pack/pack-*.keep EOF # After quarantined objects are migrated, validate that the ".keep" @@ -101,4 +100,29 @@ test_expect_success '.keep file is removed after push' ' test_path_is_missing "$keep" ' +test_expect_success '.keep file created outside of process is not removed' ' + test_when_finished rm -rf keep.git && + git init --bare keep.git && + + git -C keep.git config set receive.unpackLimit 0 && + + # Create a ".keep" lockfile matching the packfile to simulate a + # concurrent git-receive-pack(1) that has already written its lockfile. + test_hook -C keep.git pre-receive <<-\EOF && + pack="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.pack)" && + pack="${pack##*/}" && + echo "baz" >"objects/pack/${pack%.pack}.keep" + EOF + + test_commit bar && + test_must_fail git push keep.git HEAD && + + # The ".keep" lockfile of the concurrent git-receive-pack(1) should be + # left in its place. + keep="$(ls keep.git/objects/pack/pack-*.keep)" && + test_path_is_file "$keep" && + test_grep "baz" "$keep" && + test_path_is_missing "${keep%.keep}.pack" +' + test_done