* [PATCH v2 1/4] loose: use finite timeout when locking "loose-object-idx" file
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
@ 2026-10-29 0:24 ` Justin Tobler
2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 0:24 ` [PATCH v2 2/4] loose: lift loose object index parsing logic into helper Justin Tobler
` (4 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 0:24 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Loose object index file writers first attempt to acquire a lock via
`repo_hold_lock_file_for_update_timeout()` and specify a timeout value
of -1. With a negative timeout value, the lock acquisition retries
indefinitely while the lockfile still exists. If a process dies while
holding the lock and leaves a stale lockfile behind, every subsequent
writer of the mapping file hangs forever instead of reporting an error.
Introduce LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS to set the timeout to 1000ms
and use it for all callers that lock a "loose-object-idx" file. A value
of 1000ms matches the default timeout used for "core.packedRefsTimeout"
and should provide concurrent writers sufficient time to acquire the
lock.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/loose.c b/loose.c
index d6a2bb5ba3..c6f4ee0ec6 100644
--- a/loose.c
+++ b/loose.c
@@ -14,6 +14,8 @@
static const char *loose_object_header = "# loose-object-idx\n";
+#define LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS 1000
+
static inline int should_use_loose_object_map(struct repository *repo)
{
return repo->compat_hash_algo && repo->gitdir;
@@ -137,7 +139,8 @@ int repo_write_loose_object_map(struct repository *repo)
repo_common_path_replace(repo, &path, "objects/loose-object-idx");
fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
- LOCK_DIE_ON_ERROR, -1);
+ LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
iter = kh_begin(map);
if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)
goto errout;
@@ -180,7 +183,8 @@ static int write_one_object(struct odb_source_loose *loose,
strbuf_addf(&path, "%s/loose-object-idx", loose->base.path);
repo_hold_lock_file_for_update_timeout(loose->base.odb->repo, &lock,
- path.buf, LOCK_DIE_ON_ERROR, -1);
+ path.buf, LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v2 1/4] loose: use finite timeout when locking "loose-object-idx" file
2026-10-29 0:24 ` [PATCH v2 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
@ 2026-10-29 8:05 ` Patrick Steinhardt
0 siblings, 0 replies; 24+ messages in thread
From: Patrick Steinhardt @ 2026-10-29 8:05 UTC (permalink / raw)
To: Justin Tobler; +Cc: git
On Wed, Oct 28, 2026 at 07:24:10PM -0500, Justin Tobler wrote:
> Loose object index file writers first attempt to acquire a lock via
> `repo_hold_lock_file_for_update_timeout()` and specify a timeout value
> of -1. With a negative timeout value, the lock acquisition retries
> indefinitely while the lockfile still exists. If a process dies while
> holding the lock and leaves a stale lockfile behind, every subsequent
> writer of the mapping file hangs forever instead of reporting an error.
>
> Introduce LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS to set the timeout to 1000ms
> and use it for all callers that lock a "loose-object-idx" file. A value
> of 1000ms matches the default timeout used for "core.packedRefsTimeout"
> and should provide concurrent writers sufficient time to acquire the
> lock.
Right, aborting eventually is way better compared to blocking
indefinitely. Ideally we should also make this setting configurable. But
I think it's fine to skip that for now and fix that at a later point in
time.
Patrick
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 2/4] loose: lift loose object index parsing logic into helper
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
2026-10-29 0:24 ` [PATCH v2 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
@ 2026-10-29 0:24 ` Justin Tobler
2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 0:24 ` [PATCH v2 3/4] loose: lift loose object index writing " Justin Tobler
` (3 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 0:24 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Mapping entries from the "loose-object-idx" file are parsed via
`loose_object_map_load()` and inserted into the in-memory loose object
map of the corresponding object source. In a subsequent commit, a
temporary loose object index file must also be parsed and have its
contents stored in a map that is not attached to any object source.
Extract the logic for parsing a loose object index file into a separate
`loose_object_map_read()` helper function that allows callers to
explicitly specify the map to insert the entries into. The
`insert_loose_map()` helper function is also updated accordingly to
explicitly operate on the provided map and optional cache instead of the
object source.
While at it, stop treating all errors related to opening the
"loose-object-idx" file as success and instead only ignore ENOENT. It is
expected that a mapping file may not always be present, but other errors
should be signaled back to callers. Note that this slightly changes the
behavior of `repo_read_loose_object_map()`, which now stops loading the
mapping files of subsequent object sources when one is unreadable, the
same way it already does when one is malformed.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 87 +++++++++++++++++++++++++++++++++++----------------------
1 file changed, 54 insertions(+), 33 deletions(-)
diff --git a/loose.c b/loose.c
index c6f4ee0ec6..5f72c644f9 100644
--- a/loose.c
+++ b/loose.c
@@ -48,67 +48,88 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const
return 1;
}
-static int insert_loose_map(struct odb_source_loose *loose,
+static int insert_loose_map(struct loose_object_map *map,
+ struct oidtree *cache,
const struct object_id *oid,
const struct object_id *compat_oid)
{
- struct loose_object_map *map = loose->map;
int inserted = 0;
inserted |= insert_oid_pair(map->to_compat, oid, compat_oid);
inserted |= insert_oid_pair(map->to_storage, compat_oid, oid);
- if (inserted)
- oidtree_insert(loose->cache, compat_oid, NULL);
+ if (inserted && cache)
+ oidtree_insert(cache, compat_oid, NULL);
return inserted;
}
-int loose_object_map_load(struct odb_source_loose *loose)
+static int loose_object_map_read(struct repository *repo, const char *path,
+ struct loose_object_map *map,
+ struct oidtree *cache)
{
- struct repository *repo = loose->base.odb->repo;
struct strbuf buf = STRBUF_INIT;
- char *path;
FILE *fp;
- int ret = -1;
-
- if (!should_use_loose_object_map(repo))
- return 0;
-
- if (!loose->map)
- loose_object_map_init(&loose->map);
- if (!loose->cache) {
- ALLOC_ARRAY(loose->cache, 1);
- oidtree_init(loose->cache);
- }
-
- insert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);
- insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);
- insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);
+ int ret;
- path = xstrfmt("%s/loose-object-idx", loose->base.path);
fp = fopen(path, "rb");
if (!fp) {
- free(path);
- return 0;
+ if (errno == ENOENT)
+ return 0;
+ return -1;
}
- if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header))
- goto err;
+ if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header)) {
+ ret = -1;
+ goto out;
+ }
while (!strbuf_getline_lf(&buf, fp)) {
const char *p;
struct object_id oid, compat_oid;
if (parse_oid_hex_algop(buf.buf, &oid, &p, repo->hash_algo) ||
*p++ != ' ' ||
parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||
- p != buf.buf + buf.len)
- goto err;
- insert_loose_map(loose, &oid, &compat_oid);
+ p != buf.buf + buf.len) {
+ ret = -1;
+ goto out;
+ }
+ insert_loose_map(map, cache, &oid, &compat_oid);
}
ret = ferror(fp) ? -1 : 0;
-err:
- fclose(fp);
+
+out:
strbuf_release(&buf);
+ fclose(fp);
+
+ return ret;
+}
+
+int loose_object_map_load(struct odb_source_loose *loose)
+{
+ struct repository *repo = loose->base.odb->repo;
+ char *path;
+ int ret;
+
+ if (!should_use_loose_object_map(repo))
+ return 0;
+
+ if (!loose->map)
+ loose_object_map_init(&loose->map);
+ if (!loose->cache) {
+ ALLOC_ARRAY(loose->cache, 1);
+ oidtree_init(loose->cache);
+ }
+
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_tree,
+ repo->compat_hash_algo->empty_tree);
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_blob,
+ repo->compat_hash_algo->empty_blob);
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->null_oid,
+ repo->compat_hash_algo->null_oid);
+
+ path = xstrfmt("%s/loose-object-idx", loose->base.path);
+ ret = loose_object_map_read(repo, path, loose->map, loose->cache);
+
free(path);
return ret;
}
@@ -223,7 +244,7 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
if (!should_use_loose_object_map(loose->base.odb->repo))
return 0;
- inserted = insert_loose_map(loose, oid, compat_oid);
+ inserted = insert_loose_map(loose->map, loose->cache, oid, compat_oid);
if (inserted)
return write_one_object(loose, oid, compat_oid);
return 0;
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v2 2/4] loose: lift loose object index parsing logic into helper
2026-10-29 0:24 ` [PATCH v2 2/4] loose: lift loose object index parsing logic into helper Justin Tobler
@ 2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 21:10 ` Justin Tobler
0 siblings, 1 reply; 24+ messages in thread
From: Patrick Steinhardt @ 2026-10-29 8:05 UTC (permalink / raw)
To: Justin Tobler; +Cc: git
On Wed, Oct 28, 2026 at 07:24:11PM -0500, Justin Tobler wrote:
> diff --git a/loose.c b/loose.c
> index c6f4ee0ec6..5f72c644f9 100644
> --- a/loose.c
> +++ b/loose.c
> @@ -48,67 +48,88 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const
> return 1;
> }
>
> -static int insert_loose_map(struct odb_source_loose *loose,
> +static int insert_loose_map(struct loose_object_map *map,
> + struct oidtree *cache,
> const struct object_id *oid,
> const struct object_id *compat_oid)
> {
> - struct loose_object_map *map = loose->map;
> int inserted = 0;
>
> inserted |= insert_oid_pair(map->to_compat, oid, compat_oid);
> inserted |= insert_oid_pair(map->to_storage, compat_oid, oid);
> - if (inserted)
> - oidtree_insert(loose->cache, compat_oid, NULL);
> + if (inserted && cache)
> + oidtree_insert(cache, compat_oid, NULL);
Okay, so the `cache` is optional now. You mention that fact in the
commit message, but you don't explain why that is. I assume a subsequent
commit will make use of this.
> return inserted;
> }
>
> -int loose_object_map_load(struct odb_source_loose *loose)
> +static int loose_object_map_read(struct repository *repo, const char *path,
> + struct loose_object_map *map,
> + struct oidtree *cache)
> {
> - struct repository *repo = loose->base.odb->repo;
> struct strbuf buf = STRBUF_INIT;
> - char *path;
> FILE *fp;
> - int ret = -1;
> -
> - if (!should_use_loose_object_map(repo))
> - return 0;
> -
> - if (!loose->map)
> - loose_object_map_init(&loose->map);
> - if (!loose->cache) {
> - ALLOC_ARRAY(loose->cache, 1);
> - oidtree_init(loose->cache);
> - }
> -
> - insert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);
> - insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);
> - insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);
Okay, all of this stays in `loose_object_map_load()` further down.
> + int ret;
>
> - path = xstrfmt("%s/loose-object-idx", loose->base.path);
> fp = fopen(path, "rb");
> if (!fp) {
> - free(path);
> - return 0;
> + if (errno == ENOENT)
> + return 0;
> + return -1;
> }
>
> - if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header))
> - goto err;
> + if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header)) {
> + ret = -1;
> + goto out;
> + }
Ideally we'd of course print an error message telling the user what's
happened. But we didn't before either, so this is fine as-is.
> while (!strbuf_getline_lf(&buf, fp)) {
> const char *p;
> struct object_id oid, compat_oid;
> if (parse_oid_hex_algop(buf.buf, &oid, &p, repo->hash_algo) ||
> *p++ != ' ' ||
> parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||
> - p != buf.buf + buf.len)
> - goto err;
> - insert_loose_map(loose, &oid, &compat_oid);
> + p != buf.buf + buf.len) {
> + ret = -1;
> + goto out;
> + }
> + insert_loose_map(map, cache, &oid, &compat_oid);
> }
>
> ret = ferror(fp) ? -1 : 0;
> -err:
> - fclose(fp);
> +
> +out:
> strbuf_release(&buf);
> + fclose(fp);
> +
> + return ret;
> +}
> +
> +int loose_object_map_load(struct odb_source_loose *loose)
> +{
> + struct repository *repo = loose->base.odb->repo;
> + char *path;
> + int ret;
> +
> + if (!should_use_loose_object_map(repo))
> + return 0;
> +
> + if (!loose->map)
> + loose_object_map_init(&loose->map);
> + if (!loose->cache) {
> + ALLOC_ARRAY(loose->cache, 1);
> + oidtree_init(loose->cache);
> + }
> +
> + insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_tree,
> + repo->compat_hash_algo->empty_tree);
> + insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_blob,
> + repo->compat_hash_algo->empty_blob);
> + insert_loose_map(loose->map, loose->cache, repo->hash_algo->null_oid,
> + repo->compat_hash_algo->null_oid);
> +
> + path = xstrfmt("%s/loose-object-idx", loose->base.path);
> + ret = loose_object_map_read(repo, path, loose->map, loose->cache);
> +
> free(path);
> return ret;
> }
Right, and this here is the original function that we had with the
remaining bits that are specific to the source.
Patrick
^ permalink raw reply [flat|nested] 24+ messages in thread* Re: [PATCH v2 2/4] loose: lift loose object index parsing logic into helper
2026-10-29 8:05 ` Patrick Steinhardt
@ 2026-10-29 21:10 ` Justin Tobler
0 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 21:10 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On 26/10/29 09:05AM, Patrick Steinhardt wrote:
> On Wed, Oct 28, 2026 at 07:24:11PM -0500, Justin Tobler wrote:
> > - if (inserted)
> > - oidtree_insert(loose->cache, compat_oid, NULL);
> > + if (inserted && cache)
> > + oidtree_insert(cache, compat_oid, NULL);
>
> Okay, so the `cache` is optional now. You mention that fact in the
> commit message, but you don't explain why that is. I assume a subsequent
> commit will make use of this.
Yes, this is used in the final patch. I'll update the commit message to
make this a bit more clear.
-Justin
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 3/4] loose: lift loose object index writing logic into helper
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
2026-10-29 0:24 ` [PATCH v2 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
2026-10-29 0:24 ` [PATCH v2 2/4] loose: lift loose object index parsing logic into helper Justin Tobler
@ 2026-10-29 0:24 ` Justin Tobler
2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 0:24 ` [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir Justin Tobler
` (2 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 0:24 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Writing the in-memory loose object map to a "loose-object-idx" file is
handled via `repo_write_loose_object_map()`, which always writes the map
into the primary object directory. In a subsequent commit, a separate
"loose-object-idx" file is staged prior to commit and must be
constructed from a loose object map not attached to the primary loose
object source.
Extract the logic for writing a loose object index file into a separate
`loose_object_map_write()` helper function that allows callers to
explicitly specify both the loose object map as well as the file
descriptor to write the entries to.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 69 +++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 45 insertions(+), 24 deletions(-)
diff --git a/loose.c b/loose.c
index 5f72c644f9..f2e111c25e 100644
--- a/loose.c
+++ b/loose.c
@@ -146,25 +146,17 @@ int repo_read_loose_object_map(struct repository *repo)
return 0;
}
-int repo_write_loose_object_map(struct repository *repo)
+static int loose_object_map_write(struct repository *repo,
+ struct loose_object_map *loose_map, int fd)
{
- struct odb_source_files *files = odb_source_files_downcast(repo->objects->source);
- kh_oid_map_t *map = files->dirs->loose->map->to_compat;
- struct lock_file lock;
- int fd;
+ kh_oid_map_t *map = loose_map->to_compat;
+ struct strbuf buf = STRBUF_INIT;
khiter_t iter;
- struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
-
- if (!should_use_loose_object_map(repo))
- return 0;
+ int ret = -1;
- repo_common_path_replace(repo, &path, "objects/loose-object-idx");
- fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
- LOCK_DIE_ON_ERROR,
- LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
iter = kh_begin(map);
if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)
- goto errout;
+ goto out;
for (; iter != kh_end(map); iter++) {
if (kh_exist(map, iter)) {
@@ -173,24 +165,53 @@ int repo_write_loose_object_map(struct repository *repo)
continue;
strbuf_addf(&buf, "%s %s\n", oid_to_hex(&kh_key(map, iter)), oid_to_hex(kh_value(map, iter)));
if (write_in_full(fd, buf.buf, buf.len) < 0)
- goto errout;
+ goto out;
strbuf_reset(&buf);
}
}
+
+ ret = 0;
+
+out:
strbuf_release(&buf);
+
+ return ret;
+}
+
+int repo_write_loose_object_map(struct repository *repo)
+{
+ struct odb_source_files *files = odb_source_files_downcast(repo->objects->source);
+ struct strbuf path = STRBUF_INIT;
+ struct lock_file lock;
+ int ret;
+ int fd;
+
+ if (!should_use_loose_object_map(repo))
+ return 0;
+
+ repo_common_path_replace(repo, &path, "objects/loose-object-idx");
+ fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
+ LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
+
+ if (loose_object_map_write(repo, files->dirs->loose->map, fd) < 0) {
+ ret = error_errno(_("unable to write %s"),
+ get_lock_file_path(&lock));
+ goto out;
+ }
+
if (commit_lock_file(&lock) < 0) {
- error_errno(_("could not write loose object index %s"), path.buf);
- strbuf_release(&path);
- return -1;
+ ret = error_errno(_("unable to write %s"), path.buf);
+ goto out;
}
- strbuf_release(&path);
- return 0;
-errout:
+
+ ret = 0;
+
+out:
rollback_lock_file(&lock);
- strbuf_release(&buf);
- error_errno(_("failed to write loose object index %s"), path.buf);
strbuf_release(&path);
- return -1;
+
+ return ret;
}
static int write_one_object(struct odb_source_loose *loose,
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v2 3/4] loose: lift loose object index writing logic into helper
2026-10-29 0:24 ` [PATCH v2 3/4] loose: lift loose object index writing " Justin Tobler
@ 2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 21:18 ` Justin Tobler
0 siblings, 1 reply; 24+ messages in thread
From: Patrick Steinhardt @ 2026-10-29 8:05 UTC (permalink / raw)
To: Justin Tobler; +Cc: git
On Wed, Oct 28, 2026 at 07:24:12PM -0500, Justin Tobler wrote:
> Writing the in-memory loose object map to a "loose-object-idx" file is
> handled via `repo_write_loose_object_map()`, which always writes the map
> into the primary object directory. In a subsequent commit, a separate
> "loose-object-idx" file is staged prior to commit and must be
> constructed from a loose object map not attached to the primary loose
> object source.
>
> Extract the logic for writing a loose object index file into a separate
> `loose_object_map_write()` helper function that allows callers to
> explicitly specify both the loose object map as well as the file
> descriptor to write the entries to.
Okay. This commit is essentially the equivalent to the preceding one,
but for writes.
> diff --git a/loose.c b/loose.c
> index 5f72c644f9..f2e111c25e 100644
> --- a/loose.c
> +++ b/loose.c
> @@ -146,25 +146,17 @@ int repo_read_loose_object_map(struct repository *repo)
> return 0;
> }
>
> -int repo_write_loose_object_map(struct repository *repo)
> +static int loose_object_map_write(struct repository *repo,
> + struct loose_object_map *loose_map, int fd)
Curious that you're using a file descriptor instead of a path as you did
in the preceding commit. But I guess that's because of how we first have
to lock the file, which we of course don't have to do on the writing
side.
I guess we could've also moved locking into this function here to make
both interfaces match, but maybe the next commit will give a hint why we
don't.
Patrick
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 3/4] loose: lift loose object index writing logic into helper
2026-10-29 8:05 ` Patrick Steinhardt
@ 2026-10-29 21:18 ` Justin Tobler
0 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 21:18 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On 26/10/29 09:05AM, Patrick Steinhardt wrote:
> On Wed, Oct 28, 2026 at 07:24:12PM -0500, Justin Tobler wrote:
> > -int repo_write_loose_object_map(struct repository *repo)
> > +static int loose_object_map_write(struct repository *repo,
> > + struct loose_object_map *loose_map, int fd)
>
> Curious that you're using a file descriptor instead of a path as you did
> in the preceding commit. But I guess that's because of how we first have
> to lock the file, which we of course don't have to do on the writing
> side.
That is correct. When we lock the file, it gives us back the file
descriptor back for the lockfile. I decided to use the file descriptor
here because it was readily available to pass around.
> I guess we could've also moved locking into this function here to make
> both interfaces match, but maybe the next commit will give a hint why we
> don't.
Yup, this becomes more apparent in the next commit, but we don't want to
acquire the lock in the helper, so that callers can lock the file prior
to reading any preexisting version to get a consistent view. I may leave
a note about this in the next versions commit message.
-Justin
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
` (2 preceding siblings ...)
2026-10-29 0:24 ` [PATCH v2 3/4] loose: lift loose object index writing " Justin Tobler
@ 2026-10-29 0:24 ` Justin Tobler
2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 22:22 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Junio C Hamano
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
5 siblings, 1 reply; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 0:24 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
When a repository is configured with a compatibility hash, each loose
object write also records a mapping between the object and its
compatibility hash in a "loose-object-idx" file. This mapping file is
located in the same object directory the object is written to. If batch
fsync of loose objects is also enabled, the loose objects and mapping
file are instead written to a temporary object directory and expected to
be migrated to the primary object directory when the batch transaction
commits.
While migrating the loose objects works, the "loose-object-idx" file is
unable to be migrated if a mapping file already exists in the primary
object directory. This occurs because `finalize_object_file_flags()`
expects an already existing file to have the same contents. Otherwise,
the following error is returned:
error: files '.git/objects/tmp_objdir-bulk-fsync-XXXXXX/loose-object-idx' and '.git/objects/loose-object-idx' differ in contents
error: unable to migrate temporary objects
Furthermore, it is not sufficient to just replace the preexisting
mapping file with the new one as the new file only contains mapping
entries for the newly written objects.
Update the temporary directory migration logic to handle
"loose-object-idx" files explicitly by merging new entries and
atomically writing the resulting mapping file.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 47 +++++++++++++++++++++++++++++++++++
loose.h | 2 ++
t/t1016-compatObjectFormat.sh | 27 ++++++++++++++++++++
tmp-objdir.c | 11 +++++++-
4 files changed, 86 insertions(+), 1 deletion(-)
diff --git a/loose.c b/loose.c
index f2e111c25e..36da63684f 100644
--- a/loose.c
+++ b/loose.c
@@ -311,3 +311,50 @@ void loose_object_map_clear(struct loose_object_map **map)
free(m);
*map = NULL;
}
+
+int loose_object_idx_merge(struct repository *repo, const char *src,
+ const char *dst)
+{
+ struct lock_file lock = LOCK_INIT;
+ struct loose_object_map *map;
+ int ret;
+ int fd;
+
+ loose_object_map_init(&map);
+
+ fd = repo_hold_lock_file_for_update_timeout(repo, &lock, dst, 0,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
+ if (fd < 0) {
+ ret = error_errno(_("unable to lock %s"), dst);
+ goto out;
+ }
+
+ if (loose_object_map_read(repo, dst, map, NULL) < 0) {
+ ret = error(_("unable to read loose object index %s"), dst);
+ goto out;
+ }
+
+ if (loose_object_map_read(repo, src, map, NULL) < 0) {
+ ret = error(_("unable to read loose object index %s"), src);
+ goto out;
+ }
+
+ if (loose_object_map_write(repo, map, fd) < 0) {
+ ret = error_errno(_("unable to write %s"),
+ get_lock_file_path(&lock));
+ goto out;
+ }
+
+ if (commit_lock_file(&lock) < 0) {
+ ret = error_errno(_("unable to write %s"), dst);
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ loose_object_map_clear(&map);
+ rollback_lock_file(&lock);
+
+ return ret;
+}
diff --git a/loose.h b/loose.h
index ed663ac550..35b46f4be5 100644
--- a/loose.h
+++ b/loose.h
@@ -23,5 +23,7 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
const struct object_id *compat_oid);
int repo_read_loose_object_map(struct repository *repo);
int repo_write_loose_object_map(struct repository *repo);
+int loose_object_idx_merge(struct repository *repo, const char *src,
+ const char *dst);
#endif
diff --git a/t/t1016-compatObjectFormat.sh b/t/t1016-compatObjectFormat.sh
index 9cafcee509..6d0f48226f 100755
--- a/t/t1016-compatObjectFormat.sh
+++ b/t/t1016-compatObjectFormat.sh
@@ -305,4 +305,31 @@ compare_oids GPG2 'commit' signedcommit4 "$signedcommit4_sha1_oid" "$signedcommi
compare_oids GPG2 'tag' signedtag3 "$signedtag3_sha1_oid" "$signedtag3_sha256_oid"
compare_oids GPG2 'tag' signedtag4 "$signedtag4_sha1_oid" "$signedtag4_sha256_oid"
+test_expect_success 'batched fsync migrates loose object index' '
+ test_when_finished "rm -rf batch-fsync" &&
+ git init --object-format=sha1 batch-fsync &&
+ (
+ cd batch-fsync &&
+ git config core.repositoryFormatVersion 1 &&
+ git config extensions.compatObjectFormat sha256 &&
+ git config core.fsync loose-object &&
+ git config core.fsyncMethod batch &&
+
+ # Write object to generate initial loose object index.
+ echo foo >foo &&
+ git add foo &&
+ test_grep "$(git rev-parse :foo)" .git/objects/loose-object-idx &&
+
+ # Write some additional objects and ensure the corresponding
+ # temporary loose object index entries stored in the transaction
+ # are merged into the primary object directory.
+ echo bar >bar &&
+ echo baz >baz &&
+ git add bar baz &&
+ test_grep "$(git rev-parse :foo)" .git/objects/loose-object-idx &&
+ test_grep "$(git rev-parse :bar)" .git/objects/loose-object-idx &&
+ test_grep "$(git rev-parse :baz)" .git/objects/loose-object-idx
+ )
+'
+
test_done
diff --git a/tmp-objdir.c b/tmp-objdir.c
index 3debb60270..5307717354 100644
--- a/tmp-objdir.c
+++ b/tmp-objdir.c
@@ -4,6 +4,7 @@
#include "chdir-notify.h"
#include "dir.h"
#include "environment.h"
+#include "loose.h"
#include "object-file.h"
#include "path.h"
#include "string-list.h"
@@ -294,6 +295,11 @@ static int is_loose_object_shard(const char *name)
return strlen(name) == 2 && isxdigit(name[0]) && isxdigit(name[1]);
}
+static int is_loose_object_idx(const char *name)
+{
+ return !strcmp(name, "loose-object-idx");
+}
+
static int migrate_paths(struct tmp_objdir *t,
struct strbuf *src, struct strbuf *dst,
enum finalize_object_file_flags flags)
@@ -317,7 +323,10 @@ static int migrate_paths(struct tmp_objdir *t,
if (is_loose_object_shard(name))
flags_copy |= FOF_SKIP_COLLISION_CHECK;
- ret |= migrate_one(t, src, dst, flags_copy);
+ if (is_loose_object_idx(name))
+ ret |= loose_object_idx_merge(t->repo, src->buf, dst->buf);
+ else
+ ret |= migrate_one(t, src, dst, flags_copy);
strbuf_setlen(src, src_len);
strbuf_setlen(dst, dst_len);
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* Re: [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir
2026-10-29 0:24 ` [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir Justin Tobler
@ 2026-10-29 8:05 ` Patrick Steinhardt
2026-10-29 21:20 ` Justin Tobler
0 siblings, 1 reply; 24+ messages in thread
From: Patrick Steinhardt @ 2026-10-29 8:05 UTC (permalink / raw)
To: Justin Tobler; +Cc: git
On Wed, Oct 28, 2026 at 07:24:13PM -0500, Justin Tobler wrote:
> diff --git a/loose.c b/loose.c
> index f2e111c25e..36da63684f 100644
> --- a/loose.c
> +++ b/loose.c
> @@ -311,3 +311,50 @@ void loose_object_map_clear(struct loose_object_map **map)
> free(m);
> *map = NULL;
> }
> +
> +int loose_object_idx_merge(struct repository *repo, const char *src,
> + const char *dst)
> +{
> + struct lock_file lock = LOCK_INIT;
> + struct loose_object_map *map;
> + int ret;
> + int fd;
> +
> + loose_object_map_init(&map);
> +
> + fd = repo_hold_lock_file_for_update_timeout(repo, &lock, dst, 0,
> + LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
> + if (fd < 0) {
> + ret = error_errno(_("unable to lock %s"), dst);
> + goto out;
> + }
Okay, so this explains why you didn't want to pass a path to
`loose_object_map_write()`: we need the lock to span across multiple
other operations. Makes sense.
> + if (loose_object_map_read(repo, dst, map, NULL) < 0) {
> + ret = error(_("unable to read loose object index %s"), dst);
> + goto out;
> + }
> +
> + if (loose_object_map_read(repo, src, map, NULL) < 0) {
> + ret = error(_("unable to read loose object index %s"), src);
> + goto out;
> + }
It makes sense that we read `dst` under the lock so that we don't risk
losing data that a concurrent process writes into it. But to minimize
the critical section, can't we move `src` out of it and read it before
we lock?
> diff --git a/loose.h b/loose.h
> index ed663ac550..35b46f4be5 100644
> --- a/loose.h
> +++ b/loose.h
> @@ -23,5 +23,7 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
> const struct object_id *compat_oid);
> int repo_read_loose_object_map(struct repository *repo);
> int repo_write_loose_object_map(struct repository *repo);
> +int loose_object_idx_merge(struct repository *repo, const char *src,
> + const char *dst);
Do we maybe want to add a comment to this new function?
Thanks!
Patrick
^ permalink raw reply [flat|nested] 24+ messages in thread* Re: [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir
2026-10-29 8:05 ` Patrick Steinhardt
@ 2026-10-29 21:20 ` Justin Tobler
0 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 21:20 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On 26/10/29 09:05AM, Patrick Steinhardt wrote:
> On Wed, Oct 28, 2026 at 07:24:13PM -0500, Justin Tobler wrote:
> > + if (loose_object_map_read(repo, dst, map, NULL) < 0) {
> > + ret = error(_("unable to read loose object index %s"), dst);
> > + goto out;
> > + }
> > +
> > + if (loose_object_map_read(repo, src, map, NULL) < 0) {
> > + ret = error(_("unable to read loose object index %s"), src);
> > + goto out;
> > + }
>
> It makes sense that we read `dst` under the lock so that we don't risk
> losing data that a concurrent process writes into it. But to minimize
> the critical section, can't we move `src` out of it and read it before
> we lock?
Ya good point. The src file can be read prior to acquiring the lock.
Will update.
> > diff --git a/loose.h b/loose.h
> > index ed663ac550..35b46f4be5 100644
> > --- a/loose.h
> > +++ b/loose.h
> > @@ -23,5 +23,7 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
> > const struct object_id *compat_oid);
> > int repo_read_loose_object_map(struct repository *repo);
> > int repo_write_loose_object_map(struct repository *repo);
> > +int loose_object_idx_merge(struct repository *repo, const char *src,
> > + const char *dst);
>
> Do we maybe want to add a comment to this new function?
Will do!
Thanks,
-Justin
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
` (3 preceding siblings ...)
2026-10-29 0:24 ` [PATCH v2 4/4] tmp-objdir: merge loose object index when migrating tmpdir Justin Tobler
@ 2026-10-29 22:22 ` Junio C Hamano
2026-10-29 22:45 ` Justin Tobler
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
5 siblings, 1 reply; 24+ messages in thread
From: Junio C Hamano @ 2026-10-29 22:22 UTC (permalink / raw)
To: Justin Tobler; +Cc: git, ps
Justin Tobler <jltobler@gmail.com> writes:
> Greetings,
>
> While working on a separate patch series to convert the "files" ODB
> transaction to be eagerly created instead of lazily, I encountered this
> bug that causes an ODB transaction to fail on commit when a
> compatibility hash is also configured. This occurs because the temporary
> directory migration code does not know how to handle the loose object
> index file.
>
> This patch addresses updating the migration logic to explicitly merge
> mapping entries in the transaction and atomically write a new
> "loose-object-idx" file.
>
> Some other approaches I considered:
> - Storing the uncommitted object mapping entries in the transaction
> without writing a file and only writing on transaction commit. I
> chose to keep writing the temp index into the transaction so that
> child processes that write to the tempdir could still append entries.
> - Also writing mapping entires to the main ODB loose object index.
> This also didn't seem like a good apprach because it write entries
> that may not ultimately get committed.
>
> Changes since V1:
> - The loose object index write after the merging maps is now atomic.
> - Added a fixed timeout to all loose object index writers.
> - Duplicate entries are now dropped when writing the merged mapping file.
>
> Thanks for the review,
> -Justin
Thanks.
Merging this topic to 'seen' had a funny interaction with other
topics in flight. repo_write_loose_object_map() function, from
which loose_object_map_write() helper function was extracted out of,
loses all callers, and will disappear in the merge. But the new
helper, which gained a caller loose_object_idx_merge() in this
series, remains.
^ permalink raw reply [flat|nested] 24+ messages in thread* Re: [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync
2026-10-29 22:22 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Junio C Hamano
@ 2026-10-29 22:45 ` Justin Tobler
0 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 22:45 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, ps
On 26/10/29 03:22PM, Junio C Hamano wrote:
> Merging this topic to 'seen' had a funny interaction with other
> topics in flight. repo_write_loose_object_map() function, from
> which loose_object_map_write() helper function was extracted out of,
> loses all callers, and will disappear in the merge. But the new
> helper, which gained a caller loose_object_idx_merge() in this
> series, remains.
Thanks for the heads up. I see the function gets dropped by de2910a42b
(loose: drop unused `repo_write_loose_object_map()`, 2026-10-27).
Keeping the new helper and dropping `repo_write_loose_object_map()` is
indeed the correct resolution.
It does look like in `seen` the declaration of
`repo_write_loose_object_map()` is still left behind in "loose.h"
though.
I have a few small changes queued up locally for a v3, which I will send
out soon.
Thanks,
-Justin
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v3 0/4] loose: fix migrating "loose-object-idx" with batch fsync
2026-10-29 0:24 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Justin Tobler
` (4 preceding siblings ...)
2026-10-29 22:22 ` [PATCH v2 0/4] loose: fix migrating "loose-object-idx" with batch fsync Junio C Hamano
@ 2026-10-29 23:22 ` Justin Tobler
2026-10-29 23:22 ` [PATCH v3 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
` (3 more replies)
5 siblings, 4 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 23:22 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Greetings,
While working on a separate patch series to convert the "files" ODB
transaction to be eagerly created instead of lazily, I encountered this
bug that causes an ODB transaction to fail on commit when a
compatibility hash is also configured. This occurs because the temporary
directory migration code does not know how to handle the loose object
index file.
This patch addresses updating the migration logic to explicitly merge
mapping entries in the transaction and atomically write a new
"loose-object-idx" file.
Some other approaches I considered:
- Storing the uncommitted object mapping entries in the transaction
without writing a file and only writing on transaction commit. I
chose to keep writing the temp index into the transaction so that
child processes that write to the tempdir could still append entries.
- Also writing mapping entires to the main ODB loose object index.
This also didn't seem like a good apprach because it write entries
that may not ultimately get committed.
Changes since V2:
- Reading the src mapping file is now done prior to acquiring the lock
to reduce the critical window.
- Added a comment to `loose_object_idx_merge()` and updated some
commit messages to improve clarity.
Changes since V1:
- The loose object index write after the merging maps is now atomic.
- Added a fixed timeout to all loose object index writers.
- Duplicate entries are now dropped when writing the merged mapping file.
Thanks for the review,
-Justin
Justin Tobler (4):
loose: use finite timeout when locking "loose-object-idx" file
loose: lift loose object index parsing logic into helper
loose: lift loose object index writing logic into helper
tmp-objdir: merge loose object index when migrating tmpdir
loose.c | 207 ++++++++++++++++++++++++----------
loose.h | 9 ++
t/t1016-compatObjectFormat.sh | 27 +++++
tmp-objdir.c | 11 +-
4 files changed, 196 insertions(+), 58 deletions(-)
Range-diff against v2:
1: 6f104c482e = 1: 6f104c482e loose: use finite timeout when locking "loose-object-idx" file
2: 4799421a74 ! 2: 3ae068f364 loose: lift loose object index parsing logic into helper
@@ Commit message
explicitly specify the map to insert the entries into. The
`insert_loose_map()` helper function is also updated accordingly to
explicitly operate on the provided map and optional cache instead of the
- object source.
+ object source. The cache is made optional for future callers that may
+ only be interested in updating the map.
While at it, stop treating all errors related to opening the
"loose-object-idx" file as success and instead only ignore ENOENT. It is
3: 4233e913ce ! 3: 0a05eb5fb6 loose: lift loose object index writing logic into helper
@@ Commit message
Extract the logic for writing a loose object index file into a separate
`loose_object_map_write()` helper function that allows callers to
explicitly specify both the loose object map as well as the file
- descriptor to write the entries to.
+ descriptor to write the entries to. Note that the responsibility of
+ locking the file being written to is left to callers so that it is
+ possible for a lock to be held for longer than just this single
+ operation.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
4: 614aab27d1 ! 4: 3c4a3332a5 tmp-objdir: merge loose object index when migrating tmpdir
@@ loose.c: void loose_object_map_clear(struct loose_object_map **map)
+
+ loose_object_map_init(&map);
+
++ if (loose_object_map_read(repo, src, map, NULL) < 0) {
++ ret = error(_("unable to read loose object index %s"), src);
++ goto out;
++ }
++
+ fd = repo_hold_lock_file_for_update_timeout(repo, &lock, dst, 0,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
+ if (fd < 0) {
@@ loose.c: void loose_object_map_clear(struct loose_object_map **map)
+ goto out;
+ }
+
-+ if (loose_object_map_read(repo, src, map, NULL) < 0) {
-+ ret = error(_("unable to read loose object index %s"), src);
-+ goto out;
-+ }
-+
+ if (loose_object_map_write(repo, map, fd) < 0) {
+ ret = error_errno(_("unable to write %s"),
+ get_lock_file_path(&lock));
@@ loose.c: void loose_object_map_clear(struct loose_object_map **map)
## loose.h ##
@@ loose.h: int repo_add_loose_object_map(struct odb_source_loose *loose,
- const struct object_id *compat_oid);
int repo_read_loose_object_map(struct repository *repo);
int repo_write_loose_object_map(struct repository *repo);
+
++/*
++ * Merge the mapping entries of the loose object index at `src` into the one at
++ * `dst`. The resulting mapping file is written atomically.
++ *
++ * Returns 0 on success, a negative error code otherwise.
++ */
+int loose_object_idx_merge(struct repository *repo, const char *src,
+ const char *dst);
-
++
#endif
## t/t1016-compatObjectFormat.sh ##
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
--
2.56.0.173.gf57c9a7388
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH v3 1/4] loose: use finite timeout when locking "loose-object-idx" file
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
@ 2026-10-29 23:22 ` Justin Tobler
2026-10-29 23:22 ` [PATCH v3 2/4] loose: lift loose object index parsing logic into helper Justin Tobler
` (2 subsequent siblings)
3 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 23:22 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Loose object index file writers first attempt to acquire a lock via
`repo_hold_lock_file_for_update_timeout()` and specify a timeout value
of -1. With a negative timeout value, the lock acquisition retries
indefinitely while the lockfile still exists. If a process dies while
holding the lock and leaves a stale lockfile behind, every subsequent
writer of the mapping file hangs forever instead of reporting an error.
Introduce LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS to set the timeout to 1000ms
and use it for all callers that lock a "loose-object-idx" file. A value
of 1000ms matches the default timeout used for "core.packedRefsTimeout"
and should provide concurrent writers sufficient time to acquire the
lock.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/loose.c b/loose.c
index d6a2bb5ba3..c6f4ee0ec6 100644
--- a/loose.c
+++ b/loose.c
@@ -14,6 +14,8 @@
static const char *loose_object_header = "# loose-object-idx\n";
+#define LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS 1000
+
static inline int should_use_loose_object_map(struct repository *repo)
{
return repo->compat_hash_algo && repo->gitdir;
@@ -137,7 +139,8 @@ int repo_write_loose_object_map(struct repository *repo)
repo_common_path_replace(repo, &path, "objects/loose-object-idx");
fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
- LOCK_DIE_ON_ERROR, -1);
+ LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
iter = kh_begin(map);
if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)
goto errout;
@@ -180,7 +183,8 @@ static int write_one_object(struct odb_source_loose *loose,
strbuf_addf(&path, "%s/loose-object-idx", loose->base.path);
repo_hold_lock_file_for_update_timeout(loose->base.odb->repo, &lock,
- path.buf, LOCK_DIE_ON_ERROR, -1);
+ path.buf, LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 2/4] loose: lift loose object index parsing logic into helper
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
2026-10-29 23:22 ` [PATCH v3 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
@ 2026-10-29 23:22 ` Justin Tobler
2026-10-29 23:22 ` [PATCH v3 3/4] loose: lift loose object index writing " Justin Tobler
2026-10-29 23:22 ` [PATCH v3 4/4] tmp-objdir: merge loose object index when migrating tmpdir Justin Tobler
3 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 23:22 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Mapping entries from the "loose-object-idx" file are parsed via
`loose_object_map_load()` and inserted into the in-memory loose object
map of the corresponding object source. In a subsequent commit, a
temporary loose object index file must also be parsed and have its
contents stored in a map that is not attached to any object source.
Extract the logic for parsing a loose object index file into a separate
`loose_object_map_read()` helper function that allows callers to
explicitly specify the map to insert the entries into. The
`insert_loose_map()` helper function is also updated accordingly to
explicitly operate on the provided map and optional cache instead of the
object source. The cache is made optional for future callers that may
only be interested in updating the map.
While at it, stop treating all errors related to opening the
"loose-object-idx" file as success and instead only ignore ENOENT. It is
expected that a mapping file may not always be present, but other errors
should be signaled back to callers. Note that this slightly changes the
behavior of `repo_read_loose_object_map()`, which now stops loading the
mapping files of subsequent object sources when one is unreadable, the
same way it already does when one is malformed.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 87 +++++++++++++++++++++++++++++++++++----------------------
1 file changed, 54 insertions(+), 33 deletions(-)
diff --git a/loose.c b/loose.c
index c6f4ee0ec6..5f72c644f9 100644
--- a/loose.c
+++ b/loose.c
@@ -48,67 +48,88 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const
return 1;
}
-static int insert_loose_map(struct odb_source_loose *loose,
+static int insert_loose_map(struct loose_object_map *map,
+ struct oidtree *cache,
const struct object_id *oid,
const struct object_id *compat_oid)
{
- struct loose_object_map *map = loose->map;
int inserted = 0;
inserted |= insert_oid_pair(map->to_compat, oid, compat_oid);
inserted |= insert_oid_pair(map->to_storage, compat_oid, oid);
- if (inserted)
- oidtree_insert(loose->cache, compat_oid, NULL);
+ if (inserted && cache)
+ oidtree_insert(cache, compat_oid, NULL);
return inserted;
}
-int loose_object_map_load(struct odb_source_loose *loose)
+static int loose_object_map_read(struct repository *repo, const char *path,
+ struct loose_object_map *map,
+ struct oidtree *cache)
{
- struct repository *repo = loose->base.odb->repo;
struct strbuf buf = STRBUF_INIT;
- char *path;
FILE *fp;
- int ret = -1;
-
- if (!should_use_loose_object_map(repo))
- return 0;
-
- if (!loose->map)
- loose_object_map_init(&loose->map);
- if (!loose->cache) {
- ALLOC_ARRAY(loose->cache, 1);
- oidtree_init(loose->cache);
- }
-
- insert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);
- insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);
- insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);
+ int ret;
- path = xstrfmt("%s/loose-object-idx", loose->base.path);
fp = fopen(path, "rb");
if (!fp) {
- free(path);
- return 0;
+ if (errno == ENOENT)
+ return 0;
+ return -1;
}
- if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header))
- goto err;
+ if (strbuf_getwholeline(&buf, fp, '\n') || strcmp(buf.buf, loose_object_header)) {
+ ret = -1;
+ goto out;
+ }
while (!strbuf_getline_lf(&buf, fp)) {
const char *p;
struct object_id oid, compat_oid;
if (parse_oid_hex_algop(buf.buf, &oid, &p, repo->hash_algo) ||
*p++ != ' ' ||
parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||
- p != buf.buf + buf.len)
- goto err;
- insert_loose_map(loose, &oid, &compat_oid);
+ p != buf.buf + buf.len) {
+ ret = -1;
+ goto out;
+ }
+ insert_loose_map(map, cache, &oid, &compat_oid);
}
ret = ferror(fp) ? -1 : 0;
-err:
- fclose(fp);
+
+out:
strbuf_release(&buf);
+ fclose(fp);
+
+ return ret;
+}
+
+int loose_object_map_load(struct odb_source_loose *loose)
+{
+ struct repository *repo = loose->base.odb->repo;
+ char *path;
+ int ret;
+
+ if (!should_use_loose_object_map(repo))
+ return 0;
+
+ if (!loose->map)
+ loose_object_map_init(&loose->map);
+ if (!loose->cache) {
+ ALLOC_ARRAY(loose->cache, 1);
+ oidtree_init(loose->cache);
+ }
+
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_tree,
+ repo->compat_hash_algo->empty_tree);
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->empty_blob,
+ repo->compat_hash_algo->empty_blob);
+ insert_loose_map(loose->map, loose->cache, repo->hash_algo->null_oid,
+ repo->compat_hash_algo->null_oid);
+
+ path = xstrfmt("%s/loose-object-idx", loose->base.path);
+ ret = loose_object_map_read(repo, path, loose->map, loose->cache);
+
free(path);
return ret;
}
@@ -223,7 +244,7 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
if (!should_use_loose_object_map(loose->base.odb->repo))
return 0;
- inserted = insert_loose_map(loose, oid, compat_oid);
+ inserted = insert_loose_map(loose->map, loose->cache, oid, compat_oid);
if (inserted)
return write_one_object(loose, oid, compat_oid);
return 0;
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 3/4] loose: lift loose object index writing logic into helper
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
2026-10-29 23:22 ` [PATCH v3 1/4] loose: use finite timeout when locking "loose-object-idx" file Justin Tobler
2026-10-29 23:22 ` [PATCH v3 2/4] loose: lift loose object index parsing logic into helper Justin Tobler
@ 2026-10-29 23:22 ` Justin Tobler
2026-10-29 23:22 ` [PATCH v3 4/4] tmp-objdir: merge loose object index when migrating tmpdir Justin Tobler
3 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 23:22 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
Writing the in-memory loose object map to a "loose-object-idx" file is
handled via `repo_write_loose_object_map()`, which always writes the map
into the primary object directory. In a subsequent commit, a separate
"loose-object-idx" file is staged prior to commit and must be
constructed from a loose object map not attached to the primary loose
object source.
Extract the logic for writing a loose object index file into a separate
`loose_object_map_write()` helper function that allows callers to
explicitly specify both the loose object map as well as the file
descriptor to write the entries to. Note that the responsibility of
locking the file being written to is left to callers so that it is
possible for a lock to be held for longer than just this single
operation.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 69 +++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 45 insertions(+), 24 deletions(-)
diff --git a/loose.c b/loose.c
index 5f72c644f9..f2e111c25e 100644
--- a/loose.c
+++ b/loose.c
@@ -146,25 +146,17 @@ int repo_read_loose_object_map(struct repository *repo)
return 0;
}
-int repo_write_loose_object_map(struct repository *repo)
+static int loose_object_map_write(struct repository *repo,
+ struct loose_object_map *loose_map, int fd)
{
- struct odb_source_files *files = odb_source_files_downcast(repo->objects->source);
- kh_oid_map_t *map = files->dirs->loose->map->to_compat;
- struct lock_file lock;
- int fd;
+ kh_oid_map_t *map = loose_map->to_compat;
+ struct strbuf buf = STRBUF_INIT;
khiter_t iter;
- struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;
-
- if (!should_use_loose_object_map(repo))
- return 0;
+ int ret = -1;
- repo_common_path_replace(repo, &path, "objects/loose-object-idx");
- fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
- LOCK_DIE_ON_ERROR,
- LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
iter = kh_begin(map);
if (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)
- goto errout;
+ goto out;
for (; iter != kh_end(map); iter++) {
if (kh_exist(map, iter)) {
@@ -173,24 +165,53 @@ int repo_write_loose_object_map(struct repository *repo)
continue;
strbuf_addf(&buf, "%s %s\n", oid_to_hex(&kh_key(map, iter)), oid_to_hex(kh_value(map, iter)));
if (write_in_full(fd, buf.buf, buf.len) < 0)
- goto errout;
+ goto out;
strbuf_reset(&buf);
}
}
+
+ ret = 0;
+
+out:
strbuf_release(&buf);
+
+ return ret;
+}
+
+int repo_write_loose_object_map(struct repository *repo)
+{
+ struct odb_source_files *files = odb_source_files_downcast(repo->objects->source);
+ struct strbuf path = STRBUF_INIT;
+ struct lock_file lock;
+ int ret;
+ int fd;
+
+ if (!should_use_loose_object_map(repo))
+ return 0;
+
+ repo_common_path_replace(repo, &path, "objects/loose-object-idx");
+ fd = repo_hold_lock_file_for_update_timeout(repo, &lock, path.buf,
+ LOCK_DIE_ON_ERROR,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
+
+ if (loose_object_map_write(repo, files->dirs->loose->map, fd) < 0) {
+ ret = error_errno(_("unable to write %s"),
+ get_lock_file_path(&lock));
+ goto out;
+ }
+
if (commit_lock_file(&lock) < 0) {
- error_errno(_("could not write loose object index %s"), path.buf);
- strbuf_release(&path);
- return -1;
+ ret = error_errno(_("unable to write %s"), path.buf);
+ goto out;
}
- strbuf_release(&path);
- return 0;
-errout:
+
+ ret = 0;
+
+out:
rollback_lock_file(&lock);
- strbuf_release(&buf);
- error_errno(_("failed to write loose object index %s"), path.buf);
strbuf_release(&path);
- return -1;
+
+ return ret;
}
static int write_one_object(struct odb_source_loose *loose,
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread* [PATCH v3 4/4] tmp-objdir: merge loose object index when migrating tmpdir
2026-10-29 23:22 ` [PATCH v3 " Justin Tobler
` (2 preceding siblings ...)
2026-10-29 23:22 ` [PATCH v3 3/4] loose: lift loose object index writing " Justin Tobler
@ 2026-10-29 23:22 ` Justin Tobler
3 siblings, 0 replies; 24+ messages in thread
From: Justin Tobler @ 2026-10-29 23:22 UTC (permalink / raw)
To: git; +Cc: ps, Justin Tobler
When a repository is configured with a compatibility hash, each loose
object write also records a mapping between the object and its
compatibility hash in a "loose-object-idx" file. This mapping file is
located in the same object directory the object is written to. If batch
fsync of loose objects is also enabled, the loose objects and mapping
file are instead written to a temporary object directory and expected to
be migrated to the primary object directory when the batch transaction
commits.
While migrating the loose objects works, the "loose-object-idx" file is
unable to be migrated if a mapping file already exists in the primary
object directory. This occurs because `finalize_object_file_flags()`
expects an already existing file to have the same contents. Otherwise,
the following error is returned:
error: files '.git/objects/tmp_objdir-bulk-fsync-XXXXXX/loose-object-idx' and '.git/objects/loose-object-idx' differ in contents
error: unable to migrate temporary objects
Furthermore, it is not sufficient to just replace the preexisting
mapping file with the new one as the new file only contains mapping
entries for the newly written objects.
Update the temporary directory migration logic to handle
"loose-object-idx" files explicitly by merging new entries and
atomically writing the resulting mapping file.
Signed-off-by: Justin Tobler <jltobler@gmail.com>
---
loose.c | 47 +++++++++++++++++++++++++++++++++++
loose.h | 9 +++++++
t/t1016-compatObjectFormat.sh | 27 ++++++++++++++++++++
tmp-objdir.c | 11 +++++++-
4 files changed, 93 insertions(+), 1 deletion(-)
diff --git a/loose.c b/loose.c
index f2e111c25e..0ce9c7486e 100644
--- a/loose.c
+++ b/loose.c
@@ -311,3 +311,50 @@ void loose_object_map_clear(struct loose_object_map **map)
free(m);
*map = NULL;
}
+
+int loose_object_idx_merge(struct repository *repo, const char *src,
+ const char *dst)
+{
+ struct lock_file lock = LOCK_INIT;
+ struct loose_object_map *map;
+ int ret;
+ int fd;
+
+ loose_object_map_init(&map);
+
+ if (loose_object_map_read(repo, src, map, NULL) < 0) {
+ ret = error(_("unable to read loose object index %s"), src);
+ goto out;
+ }
+
+ fd = repo_hold_lock_file_for_update_timeout(repo, &lock, dst, 0,
+ LOOSE_OBJECT_IDX_LOCK_TIMEOUT_MS);
+ if (fd < 0) {
+ ret = error_errno(_("unable to lock %s"), dst);
+ goto out;
+ }
+
+ if (loose_object_map_read(repo, dst, map, NULL) < 0) {
+ ret = error(_("unable to read loose object index %s"), dst);
+ goto out;
+ }
+
+ if (loose_object_map_write(repo, map, fd) < 0) {
+ ret = error_errno(_("unable to write %s"),
+ get_lock_file_path(&lock));
+ goto out;
+ }
+
+ if (commit_lock_file(&lock) < 0) {
+ ret = error_errno(_("unable to write %s"), dst);
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ loose_object_map_clear(&map);
+ rollback_lock_file(&lock);
+
+ return ret;
+}
diff --git a/loose.h b/loose.h
index ed663ac550..6ad82dc0bb 100644
--- a/loose.h
+++ b/loose.h
@@ -24,4 +24,13 @@ int repo_add_loose_object_map(struct odb_source_loose *loose,
int repo_read_loose_object_map(struct repository *repo);
int repo_write_loose_object_map(struct repository *repo);
+/*
+ * Merge the mapping entries of the loose object index at `src` into the one at
+ * `dst`. The resulting mapping file is written atomically.
+ *
+ * Returns 0 on success, a negative error code otherwise.
+ */
+int loose_object_idx_merge(struct repository *repo, const char *src,
+ const char *dst);
+
#endif
diff --git a/t/t1016-compatObjectFormat.sh b/t/t1016-compatObjectFormat.sh
index 9cafcee509..6d0f48226f 100755
--- a/t/t1016-compatObjectFormat.sh
+++ b/t/t1016-compatObjectFormat.sh
@@ -305,4 +305,31 @@ compare_oids GPG2 'commit' signedcommit4 "$signedcommit4_sha1_oid" "$signedcommi
compare_oids GPG2 'tag' signedtag3 "$signedtag3_sha1_oid" "$signedtag3_sha256_oid"
compare_oids GPG2 'tag' signedtag4 "$signedtag4_sha1_oid" "$signedtag4_sha256_oid"
+test_expect_success 'batched fsync migrates loose object index' '
+ test_when_finished "rm -rf batch-fsync" &&
+ git init --object-format=sha1 batch-fsync &&
+ (
+ cd batch-fsync &&
+ git config core.repositoryFormatVersion 1 &&
+ git config extensions.compatObjectFormat sha256 &&
+ git config core.fsync loose-object &&
+ git config core.fsyncMethod batch &&
+
+ # Write object to generate initial loose object index.
+ echo foo >foo &&
+ git add foo &&
+ test_grep "$(git rev-parse :foo)" .git/objects/loose-object-idx &&
+
+ # Write some additional objects and ensure the corresponding
+ # temporary loose object index entries stored in the transaction
+ # are merged into the primary object directory.
+ echo bar >bar &&
+ echo baz >baz &&
+ git add bar baz &&
+ test_grep "$(git rev-parse :foo)" .git/objects/loose-object-idx &&
+ test_grep "$(git rev-parse :bar)" .git/objects/loose-object-idx &&
+ test_grep "$(git rev-parse :baz)" .git/objects/loose-object-idx
+ )
+'
+
test_done
diff --git a/tmp-objdir.c b/tmp-objdir.c
index 3debb60270..5307717354 100644
--- a/tmp-objdir.c
+++ b/tmp-objdir.c
@@ -4,6 +4,7 @@
#include "chdir-notify.h"
#include "dir.h"
#include "environment.h"
+#include "loose.h"
#include "object-file.h"
#include "path.h"
#include "string-list.h"
@@ -294,6 +295,11 @@ static int is_loose_object_shard(const char *name)
return strlen(name) == 2 && isxdigit(name[0]) && isxdigit(name[1]);
}
+static int is_loose_object_idx(const char *name)
+{
+ return !strcmp(name, "loose-object-idx");
+}
+
static int migrate_paths(struct tmp_objdir *t,
struct strbuf *src, struct strbuf *dst,
enum finalize_object_file_flags flags)
@@ -317,7 +323,10 @@ static int migrate_paths(struct tmp_objdir *t,
if (is_loose_object_shard(name))
flags_copy |= FOF_SKIP_COLLISION_CHECK;
- ret |= migrate_one(t, src, dst, flags_copy);
+ if (is_loose_object_idx(name))
+ ret |= loose_object_idx_merge(t->repo, src->buf, dst->buf);
+ else
+ ret |= migrate_one(t, src, dst, flags_copy);
strbuf_setlen(src, src_len);
strbuf_setlen(dst, dst_len);
--
2.56.0.173.gf57c9a7388
^ permalink raw reply related [flat|nested] 24+ messages in thread