{"thread":{"id":"65786","subject":"[PATCH 1/9] setup: inline `check_and_apply_repository_format()`","startedAt":"2026-06-10T14:57:25Z","lastAt":"2026-06-25T06:36:09Z","messageCount":88,"participants":["Patrick Steinhardt","Junio C Hamano","Jeff King","Karthik Nayak","Justin Tobler"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"545154","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","threadId":"65786","inReplyTo":null,"subject":"[PATCH 0/9] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:06Z","receivedAt":"2026-06-10T14:57:24Z","isPatch":true,"body":"Hi,\n\nthis patch series is a follow-up of the discussion at [1]. It converts\nthe reference backends to always use absolute paths internally, which\nthen allows us to drop the calls to `chdir_notify_reparent()`.\n\nUnfortunately, the series has grown quite a bit larger than anticipated.\nThis is due to a couple of weirdnesses in how the reference database is\nconstructed with an \"onbranch\" condition. We essentially construct the\nrefdb twice and loose one, but we never noticed because the chdir\nnotification subsystem kept the pointer to it reachable.\n\nNote that the first couple patches that touch \"setup.c\" aren't strictly\nrequired. They are a remnant of a previous iteration where I tried to\nsolve the issue in a different way. But I ultimately figured that these\nchanges are worth it by themselves as they simplify \"setup.c\" a bit.\n\nThis series is built on top of 1ff279f340 (The 13th batch, 2026-06-09)\nwith ps/setup-centralize-odb-creation at 42b9d3dc9d (setup: construct\nobject database in `apply_repository_format()`, 2026-06-04) merged into\nit.\n\nThanks!\n\nPatrick\n\n[1]: <aifAVpxanV31KUpC@pks.im>\n\n---\nPatrick Steinhardt (9):\n      setup: inline `check_and_apply_repository_format()`\n      setup: stop applying repository format twice\n      setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n      refs: unregister reference stores from \"chdir_notify\"\n      chdir-notify: drop unused `chdir_notify_reparent()`\n      repository: free main reference database\n      refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n      refs: drop local buffer in `refs_compute_filesystem_location()`\n      refs: always use absolute paths for reference stores\n\n chdir-notify.c              | 26 ------------\n chdir-notify.h              |  6 +--\n refs.c                      | 35 ++++++++++++-----\n refs/files-backend.c        |  6 ---\n refs/packed-backend.c       |  4 +-\n refs/reftable-backend.c     |  3 --\n repository.c                |  5 +++\n setup.c                     | 96 ++++++++++++++++++---------------------------\n t/pack-refs-tests.sh        |  6 +--\n t/t0600-reffiles-backend.sh |  4 +-\n t/t1423-ref-backend.sh      |  9 +++--\n t/t5510-fetch.sh            |  2 +-\n 12 files changed, 83 insertions(+), 119 deletions(-)\n\n\n---\nbase-commit: 255322df35357168daefec8523a3cdc849edd6c1\nchange-id: 20260609-b4-pks-refs-avoid-chdir-notify-reparent-a4eaf1edbcab\n\n"},{"id":"545153","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-1-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 1/9] setup: inline `check_and_apply_repository_format()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:07Z","receivedAt":"2026-06-10T14:57:25Z","isPatch":true,"body":"We have two callsites of `check_and_apply_repository_format()`. In a\nsubsequent commit we'll want to adapt one of those callsites to change\nthe order in which we read and apply the repository format, at which\npoint the helper function will not really be a good fit for us anymore.\n\nInline the function to both of the callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 47 ++++++++++++++++-------------------------------\n 1 file changed, 16 insertions(+), 31 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex b4652651df..a9db1f2c23 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo,\n \treturn 0;\n }\n \n-/*\n- * Check the repository format version in the path found in repo_get_git_dir(repo),\n- * and die if it is a version we don't understand. Generally one would\n- * set_git_dir() before calling this, and use it only for \"are we in a valid\n- * repo?\".\n- *\n- * If successful and fmt is not NULL, fill fmt with data.\n- */\n-static void check_and_apply_repository_format(struct repository *repo,\n-\t\t\t\t\t      struct repository_format *fmt,\n-\t\t\t\t\t      enum apply_repository_format_flags flags)\n-{\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\n-\tif (!fmt)\n-\t\tfmt = &repo_fmt;\n-\n-\tcheck_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);\n-\tif (apply_repository_format(repo, fmt, flags, &err) < 0)\n-\t\tdie(\"%s\", err.buf);\n-\tstartup_info->have_repository = 1;\n-\n-\tclear_repository_format(&repo_fmt);\n-}\n-\n const char *enter_repo(struct repository *repo, const char *path, unsigned flags)\n {\n \tstatic struct strbuf validated_path = STRBUF_INIT;\n@@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags\n \t}\n \n \tif (is_git_directory(\".\")) {\n+\t\tstruct repository_format fmt = REPOSITORY_FORMAT_INIT;\n+\t\tstruct strbuf err = STRBUF_INIT;\n+\n \t\tset_git_dir(repo, \".\", 0);\n-\t\tcheck_and_apply_repository_format(repo, NULL,\n-\t\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n+\t\tcheck_repository_format_gently(\".\", &fmt, NULL);\n+\t\tif (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\t\tdie(\"%s\", err.buf);\n+\t\tstartup_info->have_repository = 1;\n+\n+\t\tclear_repository_format(&fmt);\n+\t\tstrbuf_release(&err);\n \t\treturn path;\n \t}\n \n@@ -2820,6 +2802,7 @@ int init_db(struct repository *repo,\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n \n \tif (real_git_dir) {\n \t\tstruct stat st;\n@@ -2846,9 +2829,10 @@ int init_db(struct repository *repo,\n \t * config file, so this will not fail.  What we are catching\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n-\tcheck_and_apply_repository_format(repo, &repo_fmt,\n-\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n-\n+\tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\tdie(\"%s\", err.buf);\n+\tstartup_info->have_repository = 1;\n \trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n@@ -2904,6 +2888,7 @@ int init_db(struct repository *repo,\n \t}\n \n \tclear_repository_format(&repo_fmt);\n+\tstrbuf_release(&err);\n \tfree(original_git_dir);\n \treturn 0;\n }\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545155","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-2-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 2/9] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:08Z","receivedAt":"2026-06-10T14:57:27Z","isPatch":true,"body":"When discovering the repository in \"setup.c\" we apply the final\nrepository format multiple times:\n\n  - Once via `repository_format_configure()`, where we configure the\n    repository format for both `struct repository_format` and `struct\n    repository`.\n\n  - And once via `apply_repository_format()`, where we then apply the\n    `struct repository_format` to the `struct repository` again.\n\nAs the format will be applied to the repository when applying the format\nit's thus somewhat unnecessary to also apply it to the repository when\nadapting the discovered format. The only reason we have to do this is\nbecause we call `repository_format_configure()` after we have already\napplied it.\n\nRefactor the code so that we first configure the repository format\nbefore applying it to the repository so that we can stop setting the\nhash and reference storage format multiple times.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 9 ++-------\n 1 file changed, 2 insertions(+), 7 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a9db1f2c23..2748155964 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n \treturn ret;\n }\n \n-static void repository_format_configure(struct repository *repo,\n-\t\t\t\t\tstruct repository_format *repo_fmt,\n+static void repository_format_configure(struct repository_format *repo_fmt,\n \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n {\n \tstruct default_format_config cfg = {\n@@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n \t\trepo_fmt->hash_algo = cfg.hash;\n \t}\n-\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n \n \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n \tif (repo_fmt->version >= 0 &&\n@@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n \n \t\tfree(backend);\n \t}\n-\n-\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n-\t\t\t\t    repo_fmt->ref_storage_payload);\n }\n \n int init_db(struct repository *repo,\n@@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n \t\tdie(\"%s\", err.buf);\n \tstartup_info->have_repository = 1;\n-\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545156","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-3-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 3/9] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:09Z","receivedAt":"2026-06-10T14:57:30Z","isPatch":true,"body":"When discovering a repository we eventually also apply the\n\"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\ntwo problems with that:\n\n  - We do this unconditionally, which is rather pointless: we really\n    only have to configure the repository when we have found one.\n\n  - We have already applied the repository format at that point in time,\n    so we need to manually reapply it.\n\nMove the logic around so that we only apply the environment variable\nwhen a repository was discovered. This also allows us to drop the\nexplcit call to `repo_set_ref_storage_format()` because we now adjust\nthe format before we apply it via `apply_repository_format()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 40 ++++++++++++++++++++--------------------\n 1 file changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 2748155964..7b2e50a8c5 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \tstatic struct strbuf cwd = STRBUF_INIT;\n \tstruct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;\n \tconst char *prefix = NULL;\n-\tconst char *ref_backend_uri;\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n \n \t/*\n@@ -2023,6 +2022,8 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t    startup_info->have_repository ||\n \t    /* GIT_DIR_EXPLICIT */\n \t    getenv(GIT_DIR_ENVIRONMENT)) {\n+\t\tconst char *ref_backend_uri;\n+\n \t\tif (!repo->gitdir) {\n \t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n \t\t\tif (!gitdir)\n@@ -2030,6 +2031,24 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\t\tsetup_git_env_internal(repo, gitdir);\n \t\t}\n \n+\t\t/*\n+\t\t * The env variable should override the repository config\n+\t\t * for 'extensions.refStorage'.\n+\t\t */\n+\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n+\t\tif (ref_backend_uri) {\n+\t\t\tchar *format;\n+\n+\t\t\tfree(repo_fmt.ref_storage_payload);\n+\n+\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n+\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n+\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n+\n+\t\t\tfree(format);\n+\t\t}\n+\n \t\tif (startup_info->have_repository) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n \n@@ -2057,25 +2076,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n-\t/*\n-\t * The env variable should override the repository config\n-\t * for 'extensions.refStorage'.\n-\t */\n-\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n-\tif (ref_backend_uri) {\n-\t\tchar *backend, *payload;\n-\t\tenum ref_storage_format format;\n-\n-\t\tparse_reference_uri(ref_backend_uri, &backend, &payload);\n-\t\tformat = ref_storage_format_by_name(backend);\n-\t\tif (format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\t\tdie(_(\"unknown ref storage format: '%s'\"), backend);\n-\t\trepo_set_ref_storage_format(repo, format, payload);\n-\n-\t\tfree(backend);\n-\t\tfree(payload);\n-\t}\n-\n \tsetup_original_cwd(repo);\n \n \tstrbuf_release(&dir);\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545157","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-4-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 4/9] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:10Z","receivedAt":"2026-06-10T14:57:32Z","isPatch":true,"body":"When creating reference stores we register them with the \"chdir_notify\"\nsubsystem. This is required because some of the paths we track may be\nrelative paths, so we have to reparent them in case the current working\ndirectory changes.\n\nBut while we register the reference stores, we never unregister them.\nThis can have multiple outcomes:\n\n  - For a repository's main reference database we essentially keep the\n    pointer alive. We never free that database, either, and our leak\n    checker doesn't notice because it's still registered.\n\n  - For submodule and worktree reference databases we do eventually free\n    them in `repo_clear()`, so we may keep pointers to free'd memory\n    registered. We never notice though as we don't tend to chdir around\n    in the middle of the process.\n\nWe never noticed either of these symptoms, but they are obviously bad.\n\nPartially fix those issues by unregistering the reference stores when\nreleasing them. The leak of the main reference database will be fixed in\na subsequent commit.\n\nNote that this requires us to use `chdir_notify_register()` instead of\n`chdir_notify_parent()`, as there is no infrastructure to unregister the\nlatter. It ultimately doesn't matter much though: in a subsequent commit\nwe'll drop this infrastructure completely. We merely require this step\nhere so that we can fix the memory leaks ahead of time.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c    | 22 +++++++++++++++++++---\n refs/packed-backend.c   | 16 +++++++++++++++-\n refs/reftable-backend.c | 16 +++++++++++++++-\n 3 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex a4c7858787..296981584b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n+static void files_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t     const char *old_cwd,\n+\t\t\t\t     const char *new_cwd,\n+\t\t\t\t     void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n+\tfree(refs->gitcommondir);\n+\trefs->gitcommondir = tmp;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n-\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n-\t\t\t      &refs->gitcommondir);\n+\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n \n@@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n+\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 0acde48c45..499cb55dfa 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n+static void packed_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *payload)\n+{\n+\tstruct packed_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n+\tfree(refs->path);\n+\trefs->path = tmp;\n+}\n+\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n+\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n \treturn ref_store;\n }\n \n@@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n+\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4ae22922de..8c93070677 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static void reftable_be_reparent(const char *name UNUSED,\n+\t\t\t\t const char *old_cwd,\n+\t\t\t\t const char *new_cwd,\n+\t\t\t\t void *payload)\n+{\n+\tstruct reftable_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+}\n+\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n+\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n \n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n@@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n+\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545158","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-5-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 5/9] chdir-notify: drop unused `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:11Z","receivedAt":"2026-06-10T14:57:35Z","isPatch":true,"body":"With the preceding commit we've removed all callers of\n`chdir_notify_reparent()`, so the function is unused now. Drop it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n chdir-notify.c | 26 --------------------------\n chdir-notify.h |  6 +-----\n 2 files changed, 1 insertion(+), 31 deletions(-)\n\ndiff --git a/chdir-notify.c b/chdir-notify.c\nindex f8bfe3cbef..1237a45e2e 100644\n--- a/chdir-notify.c\n+++ b/chdir-notify.c\n@@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t}\n }\n \n-static void reparent_cb(const char *name,\n-\t\t\tconst char *old_cwd,\n-\t\t\tconst char *new_cwd,\n-\t\t\tvoid *data)\n-{\n-\tchar **path = data;\n-\tchar *tmp = *path;\n-\n-\tif (!tmp)\n-\t\treturn;\n-\n-\t*path = reparent_relative_path(old_cwd, new_cwd, tmp);\n-\tfree(tmp);\n-\n-\tif (name) {\n-\t\ttrace_printf_key(&trace_setup_key,\n-\t\t\t\t \"setup: reparent %s to '%s'\",\n-\t\t\t\t name, *path);\n-\t}\n-}\n-\n-void chdir_notify_reparent(const char *name, char **path)\n-{\n-\tchdir_notify_register(name, reparent_cb, path);\n-}\n-\n int chdir_notify(const char *new_cwd)\n {\n \tstruct strbuf old_cwd = STRBUF_INIT;\ndiff --git a/chdir-notify.h b/chdir-notify.h\nindex 81eb69d846..36b4114472 100644\n--- a/chdir-notify.h\n+++ b/chdir-notify.h\n@@ -19,10 +19,7 @@\n  *   chdir_notify_register(\"description\", foo, data);\n  *\n  * In practice most callers will want to move a relative path to the new root;\n- * they can use the reparent_relative_path() helper for that. If that's all\n- * you're doing, you can also use the convenience function:\n- *\n- *   chdir_notify_reparent(\"description\", &my_path);\n+ * they can use the reparent_relative_path() helper for that.\n  *\n  * Whenever a chdir event occurs, that will update my_path (if it's relative)\n  * to adjust for the new cwd by freeing any existing string and allocating a\n@@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name,\n void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data);\n void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t\t\t     void *data);\n-void chdir_notify_reparent(const char *name, char **path);\n \n /*\n  *\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545159","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-6-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 6/9] repository: free main reference database","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:12Z","receivedAt":"2026-06-10T14:57:38Z","isPatch":true,"body":"While we release worktree and submodule reference databases when\nclearing a repository, we don't ever release the main reference\ndatabase. This memory leak went unnoticed because its pointer is\nkept alive by the \"chdir_notify\" subsystem.\n\nFix the memory leak.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex 187dd471c4..e2b5c6712b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->refs_private) {\n+\t\tref_store_release(repo->refs_private);\n+\t\tFREE_AND_NULL(repo->refs_private);\n+\t}\n+\n \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n \t\tref_store_release(e->value);\n \tstrmap_clear(&repo->submodule_ref_stores, 1);\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545160","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-7-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 7/9] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:13Z","receivedAt":"2026-06-10T14:57:39Z","isPatch":true,"body":"When we have an \"onbranch\" condition we need to ask the reference\ndatabase whether HEAD currently points at the configured branch. This\nunfortunately creates a chicken-and-egg problem:\n\n  - The reference database needs to read the configuration so that it\n    can configure itself.\n\n  - The configuration needs to construct a reference database to fully\n    parse all of its conditionals.\n\nThe way we handle this is by simply excluding \"onbranch\" conditionals\nwhen we haven't yet configured the reference database.\n\nThe mechanism for this is broken though: to verify whether or not we\nhave configured the reference database we check whether its format is\nset to `REF_STORAGE_UNKNOWN` in `include_by_branch()`. But typically,\nthe format _is_ already known at that time because we set it up during\nrepository discovery in \"setup.c\".\n\nThe consequence is that we have recursion:\n\n  1. We call `get_main_ref_store()`.\n\n  2. We don't yet have a reference store, so we call `ref_store_init()`.\n\n  3. We parse the configuration required for the reference store.\n\n  4. We eventually end up in `include_by_branch()`.\n\n  5. We have already configured the reference storage format, so we end\n     up calling `get_main_ref_store()` again.\n\nWe still haven't finished (1) though, so `get_main_ref_store()` will now\ncall `ref_store_init()` a second time. The end result is that we have\nconstructed the same reference store twice.\n\nOf course, as both reference stores would be assigned to `refs_private`,\nwe leak one of those two instances. This never surfaced as an actual\nleak though because the pointer is kept alive by the \"chdir_notify\"\nsubsystem.\n\nFor now, we can fix the issue by explicitly unsetting the reference\nstorage format before constructing it. This makes the mentioned check\ntrigger as expected, and consequently we won't end up constructing a\nsecond reference database at all. Ultimately, this means that we\nconsistently stop evaluating \"onbranch\" conditions when constructing the\nmain reference database.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 20 ++++++++++++++++++--\n 1 file changed, 18 insertions(+), 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex d3caa9a633..e69b9b8ac8 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2351,15 +2351,31 @@ void ref_store_release(struct ref_store *ref_store)\n \n struct ref_store *get_main_ref_store(struct repository *r)\n {\n+\tenum ref_storage_format format;\n+\n \tif (r->refs_private)\n \t\treturn r->refs_private;\n \n \tif (!r->gitdir)\n \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n \n-\tr->refs_private = ref_store_init(r, r->ref_storage_format,\n-\t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n+\t/*\n+\t * When constructing the reference backend we'll end up reading the Git\n+\t * configuration. This means we'll also try to evaluate \"onbranch\"\n+\t * conditions.\n+\t *\n+\t * We cannot read branches when constructing the refdb, so it is not\n+\t * possible to evaluate those conditions in the first place. To gate\n+\t * their evaluation we check whether or not the reference storage\n+\t * format has been configured -- we thus have to temporarily set it to\n+\t * UNKNOWN here so that we don't end up recursing.\n+\t */\n+\tformat = r->ref_storage_format;\n+\tr->ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n+\tr->refs_private = ref_store_init(r, format, r->gitdir, REF_STORE_ALL_CAPS);\n \tr->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);\n+\tr->ref_storage_format = format;\n+\n \treturn r->refs_private;\n }\n \n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545161","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-8-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 8/9] refs: drop local buffer in `refs_compute_filesystem_location()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:14Z","receivedAt":"2026-06-10T14:57:42Z","isPatch":true,"body":"We're using a local buffer in `refs_compute_filesystem_location()` that\nis only used so that we can fill it and then call `strbuf_realpath()` on\nits result. This roundtrip isn't necessary though: `strbuf_realpath()`\nalready knows to use a single buffer as both input and output at the\nsame time. So all this does is to add a bit of confusion and an extra\nmemory allocation.\n\nDrop the local buffer.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 8 ++------\n 1 file changed, 2 insertions(+), 6 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex e69b9b8ac8..4912510590 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -3571,8 +3571,6 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\t\t      bool *is_worktree, struct strbuf *refdir,\n \t\t\t\t      struct strbuf *ref_common_dir)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n-\n \t*is_worktree = get_common_dir_noenv(ref_common_dir, gitdir);\n \n \tif (!payload) {\n@@ -3586,8 +3584,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t}\n \n \tif (!is_absolute_path(payload)) {\n-\t\tstrbuf_addf(&sb, \"%s/%s\", ref_common_dir->buf, payload);\n-\t\tstrbuf_realpath(ref_common_dir, sb.buf, 1);\n+\t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n+\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n \t} else {\n \t\tstrbuf_realpath(ref_common_dir, payload, 1);\n \t}\n@@ -3600,6 +3598,4 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\tBUG(\"worktree path does not contain slash\");\n \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n \t}\n-\n-\tstrbuf_release(&sb);\n }\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545162","messageId":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-9-56c864b01c43@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH 9/9] refs: always use absolute paths for reference stores","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-10T14:57:15Z","receivedAt":"2026-06-10T14:57:45Z","isPatch":true,"body":"Both the \"files\" and \"reftable\" backends use\n`refs_compute_filesystem_location()` to figure out the location of both\nthe git and common directories. Depending on how the function is called\nwe may or may not return an absolute path.\n\nThere isn't really a good reason to use relative paths though. Quite on\nthe contrary, because we sometimes use relative paths we are forced to\nregister for chdir(3p) notifications via `chdir_notify_reparent()`.\n\nAdapt the function to always return absolute paths. This results in a\nuser-visible change in behaviour where we now unconditionally print\nabsolute paths in error messages. But arguably, that change in behaviour\nis acceptable and may even be good in cases where a Git command may end\nup accessing references across multiple different repositories.\n\nFurthermore, drop the calls to `chdir_notify_reparent()`, which aren't\nrequired anymore now that the paths are always absolute.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c                      | 11 ++++++++---\n refs/files-backend.c        | 22 ----------------------\n refs/packed-backend.c       | 18 +-----------------\n refs/reftable-backend.c     | 17 -----------------\n t/pack-refs-tests.sh        |  6 +++---\n t/t0600-reffiles-backend.sh |  4 ++--\n t/t1423-ref-backend.sh      |  9 ++++++---\n t/t5510-fetch.sh            |  2 +-\n 8 files changed, 21 insertions(+), 68 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 4912510590..8679677bf7 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -3579,15 +3579,16 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t * worktree path, as the 'gitdir' here is already the worktree\n \t\t * path and is different from 'commondir' denoted by 'ref_common_dir'.\n \t\t */\n+\t\tstrbuf_reset(refdir);\n \t\tstrbuf_addstr(refdir, gitdir);\n-\t\treturn;\n+\t\tgoto out;\n \t}\n \n \tif (!is_absolute_path(payload)) {\n \t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n-\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n \t} else {\n-\t\tstrbuf_realpath(ref_common_dir, payload, 1);\n+\t\tstrbuf_reset(ref_common_dir);\n+\t\tstrbuf_addstr(ref_common_dir, payload);\n \t}\n \n \tstrbuf_addbuf(refdir, ref_common_dir);\n@@ -3598,4 +3599,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\tBUG(\"worktree path does not contain slash\");\n \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n \t}\n+\n+out:\n+\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n+\tstrbuf_realpath(refdir, refdir->buf, 1);\n }\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 296981584b..762f392e67 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -21,7 +21,6 @@\n #include \"../lockfile.h\"\n #include \"../path.h\"\n #include \"../dir.h\"\n-#include \"../chdir-notify.h\"\n #include \"../setup.h\"\n #include \"../worktree.h\"\n #include \"../wrapper.h\"\n@@ -100,23 +99,6 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n-static void files_ref_store_reparent(const char *name UNUSED,\n-\t\t\t\t     const char *old_cwd,\n-\t\t\t\t     const char *new_cwd,\n-\t\t\t\t     void *payload)\n-{\n-\tstruct files_ref_store *refs = payload;\n-\tchar *tmp;\n-\n-\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n-\tfree(refs->base.gitdir);\n-\trefs->base.gitdir = tmp;\n-\n-\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n-\tfree(refs->gitcommondir);\n-\trefs->gitcommondir = tmp;\n-}\n-\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -145,10 +127,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n-\n \tstrbuf_release(&refdir);\n-\n \treturn ref_store;\n }\n \n@@ -197,7 +176,6 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n-\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 499cb55dfa..89e41a35a3 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -13,7 +13,6 @@\n #include \"packed-backend.h\"\n #include \"../iterator.h\"\n #include \"../lockfile.h\"\n-#include \"../chdir-notify.h\"\n #include \"../statinfo.h\"\n #include \"../worktree.h\"\n #include \"../wrapper.h\"\n@@ -211,19 +210,6 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n-static void packed_ref_store_reparent(const char *name UNUSED,\n-\t\t\t\t      const char *old_cwd,\n-\t\t\t\t      const char *new_cwd,\n-\t\t\t\t      void *payload)\n-{\n-\tstruct packed_ref_store *refs = payload;\n-\tchar *tmp;\n-\n-\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n-\tfree(refs->path);\n-\trefs->path = tmp;\n-}\n-\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -239,10 +225,9 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tbase_ref_store_init(ref_store, repo, gitdir, &refs_be_packed);\n \trefs->store_flags = opts->access_flags;\n-\n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n+\n \treturn ref_store;\n }\n \n@@ -287,7 +272,6 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n-\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 8c93070677..8cc1dbbbdd 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -2,7 +2,6 @@\n \n #include \"../git-compat-util.h\"\n #include \"../abspath.h\"\n-#include \"../chdir-notify.h\"\n #include \"../config.h\"\n #include \"../dir.h\"\n #include \"../environment.h\"\n@@ -365,19 +364,6 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n-static void reftable_be_reparent(const char *name UNUSED,\n-\t\t\t\t const char *old_cwd,\n-\t\t\t\t const char *new_cwd,\n-\t\t\t\t void *payload)\n-{\n-\tstruct reftable_ref_store *refs = payload;\n-\tchar *tmp;\n-\n-\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n-\tfree(refs->base.gitdir);\n-\trefs->base.gitdir = tmp;\n-}\n-\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -460,8 +446,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n-\n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n \tstrbuf_release(&ref_common_dir);\n@@ -487,7 +471,6 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n-\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\ndiff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh\nindex d76b087b09..357413ba3c 100644\n--- a/t/pack-refs-tests.sh\n+++ b/t/pack-refs-tests.sh\n@@ -237,7 +237,7 @@ test_expect_success 'reject packed-refs with unterminated line' '\n \tcp .git/packed-refs .git/packed-refs.bak &&\n \ttest_when_finished \"mv .git/packed-refs.bak .git/packed-refs\" &&\n \tprintf \"%s\" \"$HEAD refs/zzzzz\" >>.git/packed-refs &&\n-\techo \"fatal: unterminated line in .git/packed-refs: $HEAD refs/zzzzz\" >expected_err &&\n+\techo \"fatal: unterminated line in $(pwd)/.git/packed-refs: $HEAD refs/zzzzz\" >expected_err &&\n \ttest_must_fail git for-each-ref >out 2>err &&\n \ttest_cmp expected_err err\n '\n@@ -246,7 +246,7 @@ test_expect_success 'reject packed-refs containing junk' '\n \tcp .git/packed-refs .git/packed-refs.bak &&\n \ttest_when_finished \"mv .git/packed-refs.bak .git/packed-refs\" &&\n \tprintf \"%s\\n\" \"bogus content\" >>.git/packed-refs &&\n-\techo \"fatal: unexpected line in .git/packed-refs: bogus content\" >expected_err &&\n+\techo \"fatal: unexpected line in $(pwd)/.git/packed-refs: bogus content\" >expected_err &&\n \ttest_must_fail git for-each-ref >out 2>err &&\n \ttest_cmp expected_err err\n '\n@@ -255,7 +255,7 @@ test_expect_success 'reject packed-refs with a short SHA-1' '\n \tcp .git/packed-refs .git/packed-refs.bak &&\n \ttest_when_finished \"mv .git/packed-refs.bak .git/packed-refs\" &&\n \tprintf \"%.7s %s\\n\" $HEAD refs/zzzzz >>.git/packed-refs &&\n-\tprintf \"fatal: unexpected line in .git/packed-refs: %.7s %s\\n\" $HEAD refs/zzzzz >expected_err &&\n+\tprintf \"fatal: unexpected line in $(pwd)/.git/packed-refs: %.7s %s\\n\" $HEAD refs/zzzzz >expected_err &&\n \ttest_must_fail git for-each-ref >out 2>err &&\n \ttest_cmp expected_err err\n '\ndiff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\nindex 74bfa2e9ba..b17f0940c2 100755\n--- a/t/t0600-reffiles-backend.sh\n+++ b/t/t0600-reffiles-backend.sh\n@@ -96,7 +96,7 @@ test_expect_success 'non-empty directory blocks create' - <<\\EOT\n \t: >.git/$prefix/foo/bar/baz.lock &&\n \ttest_when_finished \"rm -f .git/$prefix/foo/bar/baz.lock\" &&\n \tcat >expected <<-EOF &&\n-\tfatal: cannot lock ref '$prefix/foo': there is a non-empty directory '.git/$prefix/foo' blocking reference '$prefix/foo'\n+\tfatal: cannot lock ref '$prefix/foo': there is a non-empty directory '$(pwd)/.git/$prefix/foo' blocking reference '$prefix/foo'\n \tEOF\n \tprintf \"%s\\n\" \"update $prefix/foo $C\" |\n \ttest_must_fail git update-ref --stdin 2>output.err &&\n@@ -135,7 +135,7 @@ test_expect_success 'non-empty directory blocks indirect create' - <<\\EOT\n \t: >.git/$prefix/foo/bar/baz.lock &&\n \ttest_when_finished \"rm -f .git/$prefix/foo/bar/baz.lock\" &&\n \tcat >expected <<-EOF &&\n-\tfatal: cannot lock ref '$prefix/symref': there is a non-empty directory '.git/$prefix/foo' blocking reference '$prefix/foo'\n+\tfatal: cannot lock ref '$prefix/symref': there is a non-empty directory '$(pwd)/.git/$prefix/foo' blocking reference '$prefix/foo'\n \tEOF\n \tprintf \"%s\\n\" \"update $prefix/symref $C\" |\n \ttest_must_fail git update-ref --stdin 2>output.err &&\ndiff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh\nindex fd47d77e8e..875857f2d0 100755\n--- a/t/t1423-ref-backend.sh\n+++ b/t/t1423-ref-backend.sh\n@@ -145,7 +145,8 @@ do\n \t\t\t\ttest_commit 3 &&\n \n \t\t\t\tgit refs migrate --dry-run --ref-format=$to_format >out &&\n-\t\t\t\tBACKEND_PATH=\"$dir/$(sed \"s/.* ${SQ}.git\\/\\(.*\\)${SQ}/\\1/\" out)\" &&\n+\t\t\t\tBACKEND_PATH=$(sed \"s/.* the result can be found at ${SQ}\\(.*\\)${SQ}$/\\1/\" out) &&\n+\t\t\t\ttest_path_is_dir \"$BACKEND_PATH\" &&\n \t\t\t\ttest_refs_backend . $from_format \"$to_format://$BACKEND_PATH\" \"$method\"\n \t\t\t)\n \t\t'\n@@ -160,7 +161,8 @@ do\n \t\t\t\ttest_commit 3 &&\n \n \t\t\t\tgit refs migrate --dry-run --ref-format=$to_format >out &&\n-\t\t\t\tBACKEND_PATH=\"$dir/$(sed \"s/.* ${SQ}.git\\/\\(.*\\)${SQ}/\\1/\" out)\" &&\n+\t\t\t\tBACKEND_PATH=$(sed \"s/.* the result can be found at ${SQ}\\(.*\\)${SQ}$/\\1/\" out) &&\n+\t\t\t\ttest_path_is_dir \"$BACKEND_PATH\" &&\n \n \t\t\t\ttest_refs_backend . $from_format \"$to_format://$BACKEND_PATH\" \"$method\" &&\n \n@@ -187,7 +189,8 @@ do\n \t\t\t\ttest_commit 3 &&\n \n \t\t\t\tgit refs migrate --dry-run --ref-format=$to_format >out &&\n-\t\t\t\tBACKEND_PATH=\"$dir/$(sed \"s/.* ${SQ}.git\\/\\(.*\\)${SQ}/\\1/\" out)\" &&\n+\t\t\t\tBACKEND_PATH=$(sed \"s/.* the result can be found at ${SQ}\\(.*\\)${SQ}$/\\1/\" out) &&\n+\t\t\t\ttest_path_is_dir \"$BACKEND_PATH\" &&\n \n \t\t\t\trun_with_uri . \"$from_format\" \"$to_format://$BACKEND_PATH\" \\\n \t\t\t\t\t\"worktree add ../wt 2\" \"$method\" &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex eca9a973b5..d5f84d99df 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -1741,7 +1741,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensiti\n \t\tcd case_insensitive &&\n \t\tgit remote add origin -- ../case_sensitive_df &&\n \t\ttest_must_fail git fetch -f origin \"refs/heads/*:refs/heads/*\" 2>err &&\n-\t\ttest_grep \"cannot lock ref ${SQ}refs/remotes/origin/foo${SQ}: there is a non-empty directory ${SQ}./refs/remotes/origin/foo${SQ} blocking reference ${SQ}refs/remotes/origin/foo${SQ}\" err &&\n+\t\ttest_grep \"cannot lock ref ${SQ}refs/remotes/origin/foo${SQ}: there is a non-empty directory ${SQ}$(pwd)/refs/remotes/origin/foo${SQ} blocking reference ${SQ}refs/remotes/origin/foo${SQ}\" err &&\n \t\tgit rev-parse refs/heads/main >expect &&\n \t\tgit rev-parse refs/heads/Foo/bar >actual &&\n \t\ttest_cmp expect actual\n\n-- \n2.54.0.1189.g8c84645362.dirty\n\n"},{"id":"545174","messageId":"xmqqa4t2wbb5.fsf@gitster.g","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-3-56c864b01c43@pks.im","subject":"Re: [PATCH 3/9] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-10T17:32:46Z","receivedAt":"2026-06-10T17:32:49Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When discovering a repository we eventually also apply the\n> \"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\n> two problems with that:\n>\n>   - We do this unconditionally, which is rather pointless: we really\n>     only have to configure the repository when we have found one.\n>\n>   - We have already applied the repository format at that point in time,\n>     so we need to manually reapply it.\n\nDoes the second point have a small typo, i.e., \"if we have a\nrepository, we have already applied the ref backend to it when we\ndiscovered it, so NO need to manually reapply\"?\n\n> Move the logic around so that we only apply the environment variable\n> when a repository was discovered. This also allows us to drop the\n> explcit call to `repo_set_ref_storage_format()` because we now adjust\n> the format before we apply it via `apply_repository_format()`.\n\nMakes sense.\n\n"},{"id":"545244","messageId":"20260611065346.GD2191159@coredump.intra.peff.net","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"Re: [PATCH 0/9] refs: stop using `chdir_notify_reparent()`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-11T06:53:46Z","receivedAt":"2026-06-11T06:53:48Z","isPatch":true,"body":"On Wed, Jun 10, 2026 at 04:57:06PM +0200, Patrick Steinhardt wrote:\n\n> this patch series is a follow-up of the discussion at [1]. It converts\n> the reference backends to always use absolute paths internally, which\n> then allows us to drop the calls to `chdir_notify_reparent()`.\n\nWe added chdir-notify to suport set_work_tree(). Commit 8500e0de3f\n(set_work_tree: use chdir_notify, 2018-03-30) mentions an optimization\nfrom 044bbbcb63 (Make git_dir a path relative to work_tree in\nsetup_work_tree(), 2008-06-19). That commit demonstrates some measurable\nspeedup from using relative versus absolute paths.\n\nIf we move to a world of all absolute paths where chdir-notify is not\nnecessary, will we lose that optimization?\n\nI'm not sure how much it matters in practice these days, or if those\ntimings could be repeated. And they weren't all _that_ big to start\nwith. I guess it may depend on how deep your repo is within your\nfilesystem, too.\n\n-Peff\n"},{"id":"545342","messageId":"aiukox1_HrWFxnS_@pks.im","threadId":"65786","inReplyTo":"xmqqa4t2wbb5.fsf@gitster.g","subject":"Re: [PATCH 3/9] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-12T06:18:11Z","receivedAt":"2026-06-12T06:18:17Z","isPatch":true,"body":"On Wed, Jun 10, 2026 at 10:32:46AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > When discovering a repository we eventually also apply the\n> > \"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\n> > two problems with that:\n> >\n> >   - We do this unconditionally, which is rather pointless: we really\n> >     only have to configure the repository when we have found one.\n> >\n> >   - We have already applied the repository format at that point in time,\n> >     so we need to manually reapply it.\n> \n> Does the second point have a small typo, i.e., \"if we have a\n> repository, we have already applied the ref backend to it when we\n> discovered it, so NO need to manually reapply\"?\n\nNo, this is correct as-is. At the point in time where we handle\nGIT_REFERENCE_BACKEND we have already discovered the repository format,\napplied it to the repository, configured the reference database format\net al. So because we handle GIT_REFERENCE_BACKEND _after_ that whole\ndance we basically have to re-configure the reference database format,\nwhich is awkward.\n\nPatrick\n"},{"id":"545343","messageId":"aiukqI0Nj_RRn-wZ@pks.im","threadId":"65786","inReplyTo":"20260611065346.GD2191159@coredump.intra.peff.net","subject":"Re: [PATCH 0/9] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-12T06:18:16Z","receivedAt":"2026-06-12T06:18:21Z","isPatch":true,"body":"On Thu, Jun 11, 2026 at 02:53:46AM -0400, Jeff King wrote:\n> On Wed, Jun 10, 2026 at 04:57:06PM +0200, Patrick Steinhardt wrote:\n> \n> > this patch series is a follow-up of the discussion at [1]. It converts\n> > the reference backends to always use absolute paths internally, which\n> > then allows us to drop the calls to `chdir_notify_reparent()`.\n> \n> We added chdir-notify to suport set_work_tree(). Commit 8500e0de3f\n> (set_work_tree: use chdir_notify, 2018-03-30) mentions an optimization\n> from 044bbbcb63 (Make git_dir a path relative to work_tree in\n> setup_work_tree(), 2008-06-19). That commit demonstrates some measurable\n> speedup from using relative versus absolute paths.\n\nOh, that is context I wasn't aware of. Not much of a surprise though,\ngiven that this is from 2008 :) So thanks a lot for the pointer!\n\n> If we move to a world of all absolute paths where chdir-notify is not\n> necessary, will we lose that optimization?\n\nProbably. Unfortunately, the commit doesn't have any repeatable\nbenchmarks in there, so it's hard to say whether we could still\nreproduce those issues or not.\n\n> I'm not sure how much it matters in practice these days, or if those\n> timings could be repeated. And they weren't all _that_ big to start\n> with. I guess it may depend on how deep your repo is within your\n> filesystem, too.\n\nIdeally, we'd have the best of both worlds: absolute paths everywhere\nwithout the performance hit. A while back I had a discussion with\nTorvalds on the securiy mailing list around this issue, and ultimately\nthe conclusion was that the best way forward would be to use openat(3p).\n\nThis wouldn't only allow us to optimize cases like this, but it also has\nthe added benefit that we're much less prone to TOCTOU-style issues and\nwe might even be able to use flags like O_BENEATH. So it would basically\nbe win-win. The only problem is of course that Windows doesn't have\nopenat(3p), so we'd have to emulate it, and that's where I always lost\nthe desire to do this.\n\nWhen waking up this morning though I had the thought that we shouldn't\ntry to emulate openat(3p) directly, but instead create a higher-level\ninterface.\n\n    struct fsroot;\n\n    /*\n     * Open a new filesystem root at the given directory. All subsequent\n     * calls to open will be relative to this fsroot.\n     */\n    struct fsroot *fsroot_new(const char *dir);\n\n    /*\n     * Create a new fsroot from a subdirectory relative to the given\n     * root directory.\n     */\n    struct fsroot *fsroot_new_subdir(struct fsroot *r, const char *dir);\n\n    /*\n     * Open a new file relative to the given fsroot. This will use the\n     * equivalent of O_BENEATH so that we only ever open files that are\n     * located below the fsroot.\n     */\n    int fsroot_open(struct fsroot *r, const char *path, int oflag, ...);\n\nThis is of course heavily inspired by similar interfaces that exist in\nGo [1]. By having such a higher-level abstraction it should also be way\neasier to port this to different platforms, where we can then add safety\nfeatures like O_BENEATH when available on any given platform.\n\nThe idea here would be that we can then convert some subsystems to use\nthose structures instead of tracking paths. I'd for example love for the\nrepository's working tree to use this mechanism so that we can squash a\nwhole class of potential security issues when checking out files that\nend in locations we didn't intend to.\n\nThanks!\n\nPatrick\n\n[1]: https://pkg.go.dev/io/fs#FS\n"},{"id":"545354","messageId":"CAOLa=ZQC7YCBxjxkbm8qcWqpNFgAKNpvw9B6t=+XnX4bbkGq0Q@mail.gmail.com","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-2-56c864b01c43@pks.im","subject":"Re: [PATCH 2/9] setup: stop applying repository format twice","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-12T09:00:20Z","receivedAt":"2026-06-12T09:00:22Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When discovering the repository in \"setup.c\" we apply the final\n> repository format multiple times:\n>\n>   - Once via `repository_format_configure()`, where we configure the\n>     repository format for both `struct repository_format` and `struct\n>     repository`.\n>\n>   - And once via `apply_repository_format()`, where we then apply the\n>     `struct repository_format` to the `struct repository` again.\n>\n\nOkay so we're talking applying the repository format to the `struct\nrepository` specifically.\n\n> As the format will be applied to the repository when applying the format\n> it's thus somewhat unnecessary to also apply it to the repository when\n> adapting the discovered format.\n\nThis was a bit confusing to read at first. Okay since we already apply\nthe format in the second step, the first is not necessary.\n\n> The only reason we have to do this is\n> because we call `repository_format_configure()` after we have already\n> applied it.\n\nRight, so there is a need to do this.\n\n>\n> Refactor the code so that we first configure the repository format\n> before applying it to the repository so that we can stop setting the\n> hash and reference storage format multiple times.\n>\n\nMakse sense.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  setup.c | 9 ++-------\n>  1 file changed, 2 insertions(+), 7 deletions(-)\n>\n> diff --git a/setup.c b/setup.c\n> index a9db1f2c23..2748155964 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n>  \treturn ret;\n>  }\n>\n> -static void repository_format_configure(struct repository *repo,\n> -\t\t\t\t\tstruct repository_format *repo_fmt,\n> +static void repository_format_configure(struct repository_format *repo_fmt,\n>  \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n>  {\n>  \tstruct default_format_config cfg = {\n> @@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n>  \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n>  \t\trepo_fmt->hash_algo = cfg.hash;\n>  \t}\n> -\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n>\n>  \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n>  \tif (repo_fmt->version >= 0 &&\n> @@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n>\n>  \t\tfree(backend);\n>  \t}\n> -\n> -\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n> -\t\t\t\t    repo_fmt->ref_storage_payload);\n>  }\n>\n>  int init_db(struct repository *repo,\n> @@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n>  \t * is an attempt to reinitialize new repository with an old tool.\n>  \t */\n>  \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n> +\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n>  \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n>  \t\tdie(\"%s\", err.buf);\n>  \tstartup_info->have_repository = 1;\n> -\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n>\n>  \t/*\n>  \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n>\n> --\n> 2.54.0.1189.g8c84645362.dirty\n\nThe patch looks good.\n"},{"id":"545355","messageId":"CAOLa=ZS_0b9o2YucgA6Se_Mq4nLo1Luow7adTLAifbkF9jpUrA@mail.gmail.com","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-4-56c864b01c43@pks.im","subject":"Re: [PATCH 4/9] refs: unregister reference stores from \"chdir_notify\"","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-12T09:18:28Z","receivedAt":"2026-06-12T09:18:30Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When creating reference stores we register them with the \"chdir_notify\"\n> subsystem. This is required because some of the paths we track may be\n> relative paths, so we have to reparent them in case the current working\n> directory changes.\n>\n> But while we register the reference stores, we never unregister them.\n> This can have multiple outcomes:\n>\n>   - For a repository's main reference database we essentially keep the\n>     pointer alive. We never free that database, either, and our leak\n>     checker doesn't notice because it's still registered.\n>\n>   - For submodule and worktree reference databases we do eventually free\n>     them in `repo_clear()`, so we may keep pointers to free'd memory\n>     registered. We never notice though as we don't tend to chdir around\n>     in the middle of the process.\n>\n\nSo `ref_store_release()` is what is called to release a ref_store. So\nin the former's case, we never release the ref_store even if the\nrepository is closed, wow.\n\n> We never noticed either of these symptoms, but they are obviously bad.\n>\n> Partially fix those issues by unregistering the reference stores when\n> releasing them. The leak of the main reference database will be fixed in\n> a subsequent commit.\n>\n> Note that this requires us to use `chdir_notify_register()` instead of\n> `chdir_notify_parent()`, as there is no infrastructure to unregister the\n\nShouldn't this be s/chdir_notify_parent/chdir_notify_reparent ?\n\n> latter. It ultimately doesn't matter much though: in a subsequent commit\n> we'll drop this infrastructure completely. We merely require this step\n> here so that we can fix the memory leaks ahead of time.\n\nRight, we can't unregister when using `chdir_notify_reparent()` because\nit internally calls `chdir_notify_register()` with a private cb\nfunction, and we need to supply the callback function during\nun-registering. Makes sense.\n\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/files-backend.c    | 22 +++++++++++++++++++---\n>  refs/packed-backend.c   | 16 +++++++++++++++-\n>  refs/reftable-backend.c | 16 +++++++++++++++-\n>  3 files changed, 49 insertions(+), 5 deletions(-)\n>\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index a4c7858787..296981584b 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n>  \t}\n>  }\n>\n> +static void files_ref_store_reparent(const char *name UNUSED,\n> +\t\t\t\t     const char *old_cwd,\n> +\t\t\t\t     const char *new_cwd,\n> +\t\t\t\t     void *payload)\n> +{\n> +\tstruct files_ref_store *refs = payload;\n> +\tchar *tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n> +\tfree(refs->base.gitdir);\n> +\trefs->base.gitdir = tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n> +\tfree(refs->gitcommondir);\n> +\trefs->gitcommondir = tmp;\n> +}\n> +\n\nLooks similar to `void reparent_cb()` but for both the directories.\n\n>  /*\n>   * Create a new submodule ref cache and add it to the internal\n>   * set of caches.\n> @@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n>\n>  \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n>\n> -\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n> -\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n> -\t\t\t      &refs->gitcommondir);\n> +\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n>\n>  \tstrbuf_release(&refdir);\n>\n> @@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n>  \tfree(refs->gitcommondir);\n>  \tref_store_release(refs->packed_ref_store);\n>  \tfree(refs->packed_ref_store);\n> +\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n>  }\n>\n>  static void files_reflog_path(struct files_ref_store *refs,\n> diff --git a/refs/packed-backend.c b/refs/packed-backend.c\n> index 0acde48c45..499cb55dfa 100644\n> --- a/refs/packed-backend.c\n> +++ b/refs/packed-backend.c\n> @@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n>  \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n>  }\n>\n> +static void packed_ref_store_reparent(const char *name UNUSED,\n> +\t\t\t\t      const char *old_cwd,\n> +\t\t\t\t      const char *new_cwd,\n> +\t\t\t\t      void *payload)\n> +{\n> +\tstruct packed_ref_store *refs = payload;\n> +\tchar *tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n> +\tfree(refs->path);\n> +\trefs->path = tmp;\n> +}\n> +\n>  /*\n>   * Since packed-refs is only stored in the common dir, don't parse the\n>   * payload and rely on the files-backend to set 'gitdir' correctly.\n> @@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n>\n>  \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n>  \trefs->path = strbuf_detach(&sb, NULL);\n> -\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n> +\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n>  \treturn ref_store;\n>  }\n>\n> @@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n>  \tclear_snapshot(refs);\n>  \trollback_lock_file(&refs->lock);\n>  \tdelete_tempfile(&refs->tempfile);\n> +\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n>  \tfree(refs->path);\n>  }\n>\n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 4ae22922de..8c93070677 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n>  \treturn 0;\n>  }\n>\n> +static void reftable_be_reparent(const char *name UNUSED,\n> +\t\t\t\t const char *old_cwd,\n> +\t\t\t\t const char *new_cwd,\n> +\t\t\t\t void *payload)\n> +{\n> +\tstruct reftable_ref_store *refs = payload;\n> +\tchar *tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n> +\tfree(refs->base.gitdir);\n> +\trefs->base.gitdir = tmp;\n> +}\n> +\n>  static struct ref_store *reftable_be_init(struct repository *repo,\n>  \t\t\t\t\t  const char *payload,\n>  \t\t\t\t\t  const char *gitdir,\n> @@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n>  \t\t\tgoto done;\n>  \t}\n>\n> -\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n> +\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n>\n>  done:\n>  \tassert(refs->err != REFTABLE_API_ERROR);\n> @@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n>  \t\tfree(be);\n>  \t}\n>  \tstrmap_clear(&refs->worktree_backends, 0);\n> +\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n>  }\n>\n>  static int reftable_be_create_on_disk(struct ref_store *ref_store,\n>\n> --\n> 2.54.0.1189.g8c84645362.dirty\n\nThe changes here look good.\n"},{"id":"545356","messageId":"CAOLa=ZRUF61cp5JqXKwpNLf1BO3bhMFAT5Ph_-yvz1D9-qUSzw@mail.gmail.com","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-6-56c864b01c43@pks.im","subject":"Re: [PATCH 6/9] repository: free main reference database","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-12T09:20:49Z","receivedAt":"2026-06-12T09:20:50Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While we release worktree and submodule reference databases when\n> clearing a repository, we don't ever release the main reference\n> database. This memory leak went unnoticed because its pointer is\n> kept alive by the \"chdir_notify\" subsystem.\n>\n> Fix the memory leak.\n>\n\nFunny, cause long ago I looked into this and thought I was clearly\nmissing something and eventually forgot about it. Good to know that I\nwas correct :)\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  repository.c | 5 +++++\n>  1 file changed, 5 insertions(+)\n>\n> diff --git a/repository.c b/repository.c\n> index 187dd471c4..e2b5c6712b 100644\n> --- a/repository.c\n> +++ b/repository.c\n> @@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n>  \t\tFREE_AND_NULL(repo->remote_state);\n>  \t}\n>\n> +\tif (repo->refs_private) {\n> +\t\tref_store_release(repo->refs_private);\n> +\t\tFREE_AND_NULL(repo->refs_private);\n> +\t}\n> +\n>  \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n>  \t\tref_store_release(e->value);\n>  \tstrmap_clear(&repo->submodule_ref_stores, 1);\n>\n> --\n> 2.54.0.1189.g8c84645362.dirty\n"},{"id":"545362","messageId":"CAOLa=ZR60bhH4z9ZoKTCn97QzautcihxPbTZ=_e0raMTjzajZQ@mail.gmail.com","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-9-56c864b01c43@pks.im","subject":"Re: [PATCH 9/9] refs: always use absolute paths for reference stores","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-12T09:58:19Z","receivedAt":"2026-06-12T09:58:20Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Both the \"files\" and \"reftable\" backends use\n> `refs_compute_filesystem_location()` to figure out the location of both\n> the git and common directories. Depending on how the function is called\n> we may or may not return an absolute path.\n>\n> There isn't really a good reason to use relative paths though. Quite on\n> the contrary, because we sometimes use relative paths we are forced to\n> register for chdir(3p) notifications via `chdir_notify_reparent()`.\n>\n\nWith the previous changes added, we register via\n`chdir_notify_register()`\n\n> Adapt the function to always return absolute paths. This results in a\n> user-visible change in behaviour where we now unconditionally print\n> absolute paths in error messages. But arguably, that change in behaviour\n> is acceptable and may even be good in cases where a Git command may end\n> up accessing references across multiple different repositories.\n>\n> Furthermore, drop the calls to `chdir_notify_reparent()`, which aren't\n> required anymore now that the paths are always absolute.\n>\n\nSame here, should be `chdir_notify_register()`\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs.c                      | 11 ++++++++---\n>  refs/files-backend.c        | 22 ----------------------\n>  refs/packed-backend.c       | 18 +-----------------\n>  refs/reftable-backend.c     | 17 -----------------\n>  t/pack-refs-tests.sh        |  6 +++---\n>  t/t0600-reffiles-backend.sh |  4 ++--\n>  t/t1423-ref-backend.sh      |  9 ++++++---\n>  t/t5510-fetch.sh            |  2 +-\n>  8 files changed, 21 insertions(+), 68 deletions(-)\n>\n> diff --git a/refs.c b/refs.c\n> index 4912510590..8679677bf7 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -3579,15 +3579,16 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n>  \t\t * worktree path, as the 'gitdir' here is already the worktree\n>  \t\t * path and is different from 'commondir' denoted by 'ref_common_dir'.\n>  \t\t */\n> +\t\tstrbuf_reset(refdir);\n>  \t\tstrbuf_addstr(refdir, gitdir);\n> -\t\treturn;\n> +\t\tgoto out;\n>  \t}\n>\n>  \tif (!is_absolute_path(payload)) {\n>  \t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n> -\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n>  \t} else {\n> -\t\tstrbuf_realpath(ref_common_dir, payload, 1);\n> +\t\tstrbuf_reset(ref_common_dir);\n> +\t\tstrbuf_addstr(ref_common_dir, payload);\n>  \t}\n>\n>  \tstrbuf_addbuf(refdir, ref_common_dir);\n> @@ -3598,4 +3599,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n>  \t\t\tBUG(\"worktree path does not contain slash\");\n>  \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n>  \t}\n> +\n> +out:\n> +\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n> +\tstrbuf_realpath(refdir, refdir->buf, 1);\n>  }\n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index 296981584b..762f392e67 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -21,7 +21,6 @@\n>  #include \"../lockfile.h\"\n>  #include \"../path.h\"\n>  #include \"../dir.h\"\n> -#include \"../chdir-notify.h\"\n>  #include \"../setup.h\"\n>  #include \"../worktree.h\"\n>  #include \"../wrapper.h\"\n> @@ -100,23 +99,6 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n>  \t}\n>  }\n>\n> -static void files_ref_store_reparent(const char *name UNUSED,\n> -\t\t\t\t     const char *old_cwd,\n> -\t\t\t\t     const char *new_cwd,\n> -\t\t\t\t     void *payload)\n> -{\n> -\tstruct files_ref_store *refs = payload;\n> -\tchar *tmp;\n> -\n> -\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n> -\tfree(refs->base.gitdir);\n> -\trefs->base.gitdir = tmp;\n> -\n> -\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n> -\tfree(refs->gitcommondir);\n> -\trefs->gitcommondir = tmp;\n> -}\n> -\n>  /*\n>   * Create a new submodule ref cache and add it to the internal\n>   * set of caches.\n> @@ -145,10 +127,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n>\n>  \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n>\n> -\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n> -\n>  \tstrbuf_release(&refdir);\n> -\n>  \treturn ref_store;\n>  }\n>\n> @@ -197,7 +176,6 @@ static void files_ref_store_release(struct ref_store *ref_store)\n>  \tfree(refs->gitcommondir);\n>  \tref_store_release(refs->packed_ref_store);\n>  \tfree(refs->packed_ref_store);\n> -\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n>  }\n>\n>  static void files_reflog_path(struct files_ref_store *refs,\n> diff --git a/refs/packed-backend.c b/refs/packed-backend.c\n> index 499cb55dfa..89e41a35a3 100644\n> --- a/refs/packed-backend.c\n> +++ b/refs/packed-backend.c\n> @@ -13,7 +13,6 @@\n>  #include \"packed-backend.h\"\n>  #include \"../iterator.h\"\n>  #include \"../lockfile.h\"\n> -#include \"../chdir-notify.h\"\n>  #include \"../statinfo.h\"\n>  #include \"../worktree.h\"\n>  #include \"../wrapper.h\"\n> @@ -211,19 +210,6 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n>  \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n>  }\n>\n> -static void packed_ref_store_reparent(const char *name UNUSED,\n> -\t\t\t\t      const char *old_cwd,\n> -\t\t\t\t      const char *new_cwd,\n> -\t\t\t\t      void *payload)\n> -{\n> -\tstruct packed_ref_store *refs = payload;\n> -\tchar *tmp;\n> -\n> -\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n> -\tfree(refs->path);\n> -\trefs->path = tmp;\n> -}\n> -\n>  /*\n>   * Since packed-refs is only stored in the common dir, don't parse the\n>   * payload and rely on the files-backend to set 'gitdir' correctly.\n> @@ -239,10 +225,9 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n>\n>  \tbase_ref_store_init(ref_store, repo, gitdir, &refs_be_packed);\n>  \trefs->store_flags = opts->access_flags;\n> -\n>  \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n>  \trefs->path = strbuf_detach(&sb, NULL);\n> -\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n> +\n>  \treturn ref_store;\n>  }\n>\n> @@ -287,7 +272,6 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n>  \tclear_snapshot(refs);\n>  \trollback_lock_file(&refs->lock);\n>  \tdelete_tempfile(&refs->tempfile);\n> -\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n>  \tfree(refs->path);\n>  }\n>\n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 8c93070677..8cc1dbbbdd 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -2,7 +2,6 @@\n>\n>  #include \"../git-compat-util.h\"\n>  #include \"../abspath.h\"\n> -#include \"../chdir-notify.h\"\n>  #include \"../config.h\"\n>  #include \"../dir.h\"\n>  #include \"../environment.h\"\n> @@ -365,19 +364,6 @@ static int reftable_be_config(const char *var, const char *value,\n>  \treturn 0;\n>  }\n>\n> -static void reftable_be_reparent(const char *name UNUSED,\n> -\t\t\t\t const char *old_cwd,\n> -\t\t\t\t const char *new_cwd,\n> -\t\t\t\t void *payload)\n> -{\n> -\tstruct reftable_ref_store *refs = payload;\n> -\tchar *tmp;\n> -\n> -\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n> -\tfree(refs->base.gitdir);\n> -\trefs->base.gitdir = tmp;\n> -}\n> -\n>  static struct ref_store *reftable_be_init(struct repository *repo,\n>  \t\t\t\t\t  const char *payload,\n>  \t\t\t\t\t  const char *gitdir,\n> @@ -460,8 +446,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n>  \t\t\tgoto done;\n>  \t}\n>\n> -\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n> -\n>  done:\n>  \tassert(refs->err != REFTABLE_API_ERROR);\n>  \tstrbuf_release(&ref_common_dir);\n> @@ -487,7 +471,6 @@ static void reftable_be_release(struct ref_store *ref_store)\n>  \t\tfree(be);\n>  \t}\n>  \tstrmap_clear(&refs->worktree_backends, 0);\n> -\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n>  }\n>\n>  static int reftable_be_create_on_disk(struct ref_store *ref_store,\n>\n\nThe changes look good to me.\n\n[snip]\n"},{"id":"545449","messageId":"20260613140024.GA766297@coredump.intra.peff.net","threadId":"65786","inReplyTo":"aiukqI0Nj_RRn-wZ@pks.im","subject":"Re: [PATCH 0/9] refs: stop using `chdir_notify_reparent()`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-13T14:00:24Z","receivedAt":"2026-06-13T14:00:31Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 08:18:16AM +0200, Patrick Steinhardt wrote:\n\n> > If we move to a world of all absolute paths where chdir-notify is not\n> > necessary, will we lose that optimization?\n> \n> Probably. Unfortunately, the commit doesn't have any repeatable\n> benchmarks in there, so it's hard to say whether we could still\n> reproduce those issues or not.\n\nHere's an easy-ish reproduction specific to the ref code:\n\n  rm -rf a/\n  dir=$(perl -e 'print \"a/\" x 1024')\n  mkdir -p $dir &&\n  cd $dir &&\n  git init &&\n  git commit --allow-empty -m foo &&\n  seq -f 'create refs/heads/foo%05g HEAD' 10000 |\n  git update-ref --stdin &&\n  time git show-ref\n\nBefore your series, I get timings like this:\n\n  real\t0m0.078s\n  user\t0m0.020s\n  sys\t0m0.057s\n\nAfter, I get:\n\n  real\t0m0.876s\n  user\t0m0.004s\n  sys\t0m0.872s\n\nSo it really is measurable (and I did not expect the effect to be nearly\nso large). Unsurprisingly the extra CPU goes to system time.\n\nBut obviously that case is quite silly. It's an absurdly deep hierarchy,\nand 10,000 loose refs is a lot. Just running \"git pack-refs --all\"\nbrings the before/after to roughly the same timings (around 40ms --\nfaster even than the before timing).\n\nSo it _can_ matter, but I think ultimately the better direction is\nprobably \"make fewer syscalls\". Which we do via packfiles, and via\npacked-refs, and eventually via reftables, all of which put more data\ninto a single file.\n\nI offer the script above more as food for thought, and not necessarily\nan argument against your series.\n\n> Ideally, we'd have the best of both worlds: absolute paths everywhere\n> without the performance hit. A while back I had a discussion with\n> Torvalds on the securiy mailing list around this issue, and ultimately\n> the conclusion was that the best way forward would be to use openat(3p).\n> \n> This wouldn't only allow us to optimize cases like this, but it also has\n> the added benefit that we're much less prone to TOCTOU-style issues and\n> we might even be able to use flags like O_BENEATH. So it would basically\n> be win-win. The only problem is of course that Windows doesn't have\n> openat(3p), so we'd have to emulate it, and that's where I always lost\n> the desire to do this.\n> \n> When waking up this morning though I had the thought that we shouldn't\n> try to emulate openat(3p) directly, but instead create a higher-level\n> interface.\n> [...]\n\nYeah, I think given a decent interface it might not be so bad. It would\nmean code thinking about filesystem syscalls in a different way, but if\ndone subsystem-by-subsystem it might be OK to do incrementally. Much of\nthe code that would want to switch to this is using repo_git_path() or\nsimilar already (and getting rid of those remaining static-buffer\nfunctions would be a nice bonus).\n\nI do wonder if your series here to move to absolute paths makes the\nTOCTOU situation a little worse. With a relative path, once we are\n\"inside\" the repo then we are only susceptible to changes within it.\nWhereas with an absolute path, if one of the intermediate paths changes\nfrom under us, there may be confusion.\n\nWithout thinking on it too hard, though, I'd guess if any such case is a\nsecurity problem, it already was during the \"open\" part (because it\nimplies that the attacker controls paths below you in the hierarchy, and\nyou had to get to your cwd _somehow_, at which point they could have\nattacked you then).\n\n-Peff\n"},{"id":"545550","messageId":"ai_x1eKiSC9LZM6v@pks.im","threadId":"65786","inReplyTo":"CAOLa=ZQC7YCBxjxkbm8qcWqpNFgAKNpvw9B6t=+XnX4bbkGq0Q@mail.gmail.com","subject":"Re: [PATCH 2/9] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T12:36:37Z","receivedAt":"2026-06-15T12:36:43Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 02:00:20AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > When discovering the repository in \"setup.c\" we apply the final\n> > repository format multiple times:\n> >\n> >   - Once via `repository_format_configure()`, where we configure the\n> >     repository format for both `struct repository_format` and `struct\n> >     repository`.\n> >\n> >   - And once via `apply_repository_format()`, where we then apply the\n> >     `struct repository_format` to the `struct repository` again.\n> >\n> \n> Okay so we're talking applying the repository format to the `struct\n> repository` specifically.\n> \n> > As the format will be applied to the repository when applying the format\n> > it's thus somewhat unnecessary to also apply it to the repository when\n> > adapting the discovered format.\n> \n> This was a bit confusing to read at first. Okay since we already apply\n> the format in the second step, the first is not necessary.\n\nI agree. I'll rephrase this a bit.\n\nPatrick\n"},{"id":"545551","messageId":"ai_x2uJOwInU9lvj@pks.im","threadId":"65786","inReplyTo":"CAOLa=ZS_0b9o2YucgA6Se_Mq4nLo1Luow7adTLAifbkF9jpUrA@mail.gmail.com","subject":"Re: [PATCH 4/9] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T12:36:42Z","receivedAt":"2026-06-15T12:36:47Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 02:18:28AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n[snip]\n> > We never noticed either of these symptoms, but they are obviously bad.\n> >\n> > Partially fix those issues by unregistering the reference stores when\n> > releasing them. The leak of the main reference database will be fixed in\n> > a subsequent commit.\n> >\n> > Note that this requires us to use `chdir_notify_register()` instead of\n> > `chdir_notify_parent()`, as there is no infrastructure to unregister the\n> \n> Shouldn't this be s/chdir_notify_parent/chdir_notify_reparent ?\n\nYup, good catch.\n\nPatrick\n"},{"id":"545552","messageId":"ai_x3wHDHXAGbIVX@pks.im","threadId":"65786","inReplyTo":"CAOLa=ZR60bhH4z9ZoKTCn97QzautcihxPbTZ=_e0raMTjzajZQ@mail.gmail.com","subject":"Re: [PATCH 9/9] refs: always use absolute paths for reference stores","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T12:36:47Z","receivedAt":"2026-06-15T12:36:52Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 02:58:19AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Both the \"files\" and \"reftable\" backends use\n> > `refs_compute_filesystem_location()` to figure out the location of both\n> > the git and common directories. Depending on how the function is called\n> > we may or may not return an absolute path.\n> >\n> > There isn't really a good reason to use relative paths though. Quite on\n> > the contrary, because we sometimes use relative paths we are forced to\n> > register for chdir(3p) notifications via `chdir_notify_reparent()`.\n> >\n> \n> With the previous changes added, we register via\n> `chdir_notify_register()`\n> \n> > Adapt the function to always return absolute paths. This results in a\n> > user-visible change in behaviour where we now unconditionally print\n> > absolute paths in error messages. But arguably, that change in behaviour\n> > is acceptable and may even be good in cases where a Git command may end\n> > up accessing references across multiple different repositories.\n> >\n> > Furthermore, drop the calls to `chdir_notify_reparent()`, which aren't\n> > required anymore now that the paths are always absolute.\n> >\n> \n> Same here, should be `chdir_notify_register()`\n\nYes, will fix.\n\nPatrick\n"},{"id":"545553","messageId":"ai_x5ln7JUUpJtR7@pks.im","threadId":"65786","inReplyTo":"20260613140024.GA766297@coredump.intra.peff.net","subject":"Re: [PATCH 0/9] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T12:36:54Z","receivedAt":"2026-06-15T12:36:58Z","isPatch":true,"body":"On Sat, Jun 13, 2026 at 10:00:24AM -0400, Jeff King wrote:\n> On Fri, Jun 12, 2026 at 08:18:16AM +0200, Patrick Steinhardt wrote:\n> \n> > > If we move to a world of all absolute paths where chdir-notify is not\n> > > necessary, will we lose that optimization?\n> > \n> > Probably. Unfortunately, the commit doesn't have any repeatable\n> > benchmarks in there, so it's hard to say whether we could still\n> > reproduce those issues or not.\n> \n> Here's an easy-ish reproduction specific to the ref code:\n> \n>   rm -rf a/\n>   dir=$(perl -e 'print \"a/\" x 1024')\n>   mkdir -p $dir &&\n>   cd $dir &&\n>   git init &&\n>   git commit --allow-empty -m foo &&\n>   seq -f 'create refs/heads/foo%05g HEAD' 10000 |\n>   git update-ref --stdin &&\n>   time git show-ref\n> \n> Before your series, I get timings like this:\n> \n>   real\t0m0.078s\n>   user\t0m0.020s\n>   sys\t0m0.057s\n> \n> After, I get:\n> \n>   real\t0m0.876s\n>   user\t0m0.004s\n>   sys\t0m0.872s\n> \n> So it really is measurable (and I did not expect the effect to be nearly\n> so large). Unsurprisingly the extra CPU goes to system time.\n\nThis is indeed surprisingly bad.\n\n> But obviously that case is quite silly. It's an absurdly deep hierarchy,\n> and 10,000 loose refs is a lot. Just running \"git pack-refs --all\"\n> brings the before/after to roughly the same timings (around 40ms --\n> faster even than the before timing).\n> \n> So it _can_ matter, but I think ultimately the better direction is\n> probably \"make fewer syscalls\". Which we do via packfiles, and via\n> packed-refs, and eventually via reftables, all of which put more data\n> into a single file.\n> \n> I offer the script above more as food for thought, and not necessarily\n> an argument against your series.\n\nHum, yeah. I'm a bit hesitant to just wave your findings away. I mean I\nagree with you that it's unlikely to really matter in practice. But you\nnever really know, and I'm not sure that I consider dropping the chdir\ninfra important enough to knowingly take that hit.\n\nI definitely think that we should merge the remainder of this series\nthough, as these patches simplify \"setup.c\" and fix a couple of memory\nleaks. But maybe we drop the last patch for now and...\n\n> > Ideally, we'd have the best of both worlds: absolute paths everywhere\n> > without the performance hit. A while back I had a discussion with\n> > Torvalds on the securiy mailing list around this issue, and ultimately\n> > the conclusion was that the best way forward would be to use openat(3p).\n> > \n> > This wouldn't only allow us to optimize cases like this, but it also has\n> > the added benefit that we're much less prone to TOCTOU-style issues and\n> > we might even be able to use flags like O_BENEATH. So it would basically\n> > be win-win. The only problem is of course that Windows doesn't have\n> > openat(3p), so we'd have to emulate it, and that's where I always lost\n> > the desire to do this.\n> > \n> > When waking up this morning though I had the thought that we shouldn't\n> > try to emulate openat(3p) directly, but instead create a higher-level\n> > interface.\n> > [...]\n> \n> Yeah, I think given a decent interface it might not be so bad. It would\n> mean code thinking about filesystem syscalls in a different way, but if\n> done subsystem-by-subsystem it might be OK to do incrementally. Much of\n> the code that would want to switch to this is using repo_git_path() or\n> similar already (and getting rid of those remaining static-buffer\n> functions would be a nice bonus).\n> \n> I do wonder if your series here to move to absolute paths makes the\n> TOCTOU situation a little worse. With a relative path, once we are\n> \"inside\" the repo then we are only susceptible to changes within it.\n> Whereas with an absolute path, if one of the intermediate paths changes\n> from under us, there may be confusion.\n> \n> Without thinking on it too hard, though, I'd guess if any such case is a\n> security problem, it already was during the \"open\" part (because it\n> implies that the attacker controls paths below you in the hierarchy, and\n> you had to get to your cwd _somehow_, at which point they could have\n> attacked you then).\n\n... eventually give this idea here a test?\n\nPatrick\n"},{"id":"545574","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH v2 0/8] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:46Z","receivedAt":"2026-06-15T13:56:54Z","isPatch":true,"body":"Hi,\n\nthis patch series is a follow-up of the discussion at [1]. It converts\nthe reference backends to always use absolute paths internally, which\nthen allows us to drop the calls to `chdir_notify_reparent()`.\n\nUnfortunately, the series has grown quite a bit larger than anticipated.\nThis is due to a couple of weirdnesses in how the reference database is\nconstructed with an \"onbranch\" condition. We essentially construct the\nrefdb twice and loose one, but we never noticed because the chdir\nnotification subsystem kept the pointer to it reachable.\n\nNote that the first couple patches that touch \"setup.c\" aren't strictly\nrequired. They are a remnant of a previous iteration where I tried to\nsolve the issue in a different way. But I ultimately figured that these\nchanges are worth it by themselves as they simplify \"setup.c\" a bit.\n\nThis series is built on top of 1ff279f340 (The 13th batch, 2026-06-09)\nwith ps/setup-centralize-odb-creation at 42b9d3dc9d (setup: construct\nobject database in `apply_repository_format()`, 2026-06-04) merged into\nit.\n\nChanges in v2:\n  - Drop the last patch. This seemingly destroys the whole purpose of\n    the patch series, but after Peff's hint that this is actually a\n    performance optimization I'm less inclined to drop the chdir_notify\n    infra. I still think that the remainder of the patches make sense\n    standalone, as they simplify \"setup.c\" and clean memory leaks. Going\n    forward I'd like to investigate the idea of introducing a `struct\n    fsroot` infrastructure that uses the platform-equivalent of openat\n    et al.\n  - Improve a couple of commit messages.\n  - Link to v1: https://patch.msgid.link/20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <aifAVpxanV31KUpC@pks.im>\n\n---\nPatrick Steinhardt (8):\n      setup: inline `check_and_apply_repository_format()`\n      setup: stop applying repository format twice\n      setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n      refs: unregister reference stores from \"chdir_notify\"\n      chdir-notify: drop unused `chdir_notify_reparent()`\n      repository: free main reference database\n      refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n      refs: drop local buffer in `refs_compute_filesystem_location()`\n\n chdir-notify.c          | 26 --------------\n chdir-notify.h          |  6 +---\n refs.c                  | 28 ++++++++++-----\n refs/files-backend.c    | 22 ++++++++++--\n refs/packed-backend.c   | 16 ++++++++-\n refs/reftable-backend.c | 16 ++++++++-\n repository.c            |  5 +++\n setup.c                 | 96 ++++++++++++++++++++-----------------------------\n 8 files changed, 113 insertions(+), 102 deletions(-)\n\nRange-diff versus v1:\n\n 1:  ef72346c7d =  1:  3902fecdb9 setup: inline `check_and_apply_repository_format()`\n 2:  157fc098b3 !  2:  9479ffc370 setup: stop applying repository format twice\n    @@ Commit message\n         When discovering the repository in \"setup.c\" we apply the final\n         repository format multiple times:\n     \n    -      - Once via `repository_format_configure()`, where we configure the\n    -        repository format for both `struct repository_format` and `struct\n    -        repository`.\n    +      - Once via `repository_format_configure()`, where we apply the hash\n    +        algorithm and ref storage format to both `struct repository_format`\n    +        and `struct repository`.\n     \n    -      - And once via `apply_repository_format()`, where we then apply the\n    -        `struct repository_format` to the `struct repository` again.\n    +      - And once via `apply_repository_format()`, where we apply these two\n    +        settings from `struct repository_format` to `struct repository`.\n     \n    -    As the format will be applied to the repository when applying the format\n    -    it's thus somewhat unnecessary to also apply it to the repository when\n    -    adapting the discovered format. The only reason we have to do this is\n    -    because we call `repository_format_configure()` after we have already\n    -    applied it.\n    +    With the current flow both of these are in fact necessary. But this is\n    +    only because we call `repository_format_configure()` after we have\n    +    called `apply_repository_format()`. Consequently, if we only changed the\n    +    repository format in `repository_format_configure()` it would never\n    +    propagate to the repository.\n     \n         Refactor the code so that we first configure the repository format\n         before applying it to the repository so that we can stop setting the\n 3:  f1429ae8c9 =  3:  09299c488d setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n 4:  4137f0f083 !  4:  dff1bfec7a refs: unregister reference stores from \"chdir_notify\"\n    @@ Commit message\n         a subsequent commit.\n     \n         Note that this requires us to use `chdir_notify_register()` instead of\n    -    `chdir_notify_parent()`, as there is no infrastructure to unregister the\n    +    `chdir_notify_reparent()`, as there is no infrastructure to unregister the\n         latter. It ultimately doesn't matter much though: in a subsequent commit\n         we'll drop this infrastructure completely. We merely require this step\n         here so that we can fix the memory leaks ahead of time.\n 5:  dbda87ab6a =  5:  367806c5ba chdir-notify: drop unused `chdir_notify_reparent()`\n 6:  b1d2f39def =  6:  e8eb346876 repository: free main reference database\n 7:  f7f5028a10 =  7:  090f80707c refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n 8:  818c0878f9 =  8:  14b12a8f10 refs: drop local buffer in `refs_compute_filesystem_location()`\n 9:  7408f9b69f <  -:  ---------- refs: always use absolute paths for reference stores\n\n---\nbase-commit: 255322df35357168daefec8523a3cdc849edd6c1\nchange-id: 20260609-b4-pks-refs-avoid-chdir-notify-reparent-a4eaf1edbcab\n\n"},{"id":"545575","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-1-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 1/8] setup: inline `check_and_apply_repository_format()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:47Z","receivedAt":"2026-06-15T13:56:56Z","isPatch":true,"body":"We have two callsites of `check_and_apply_repository_format()`. In a\nsubsequent commit we'll want to adapt one of those callsites to change\nthe order in which we read and apply the repository format, at which\npoint the helper function will not really be a good fit for us anymore.\n\nInline the function to both of the callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 47 ++++++++++++++++-------------------------------\n 1 file changed, 16 insertions(+), 31 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex b4652651df..a9db1f2c23 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo,\n \treturn 0;\n }\n \n-/*\n- * Check the repository format version in the path found in repo_get_git_dir(repo),\n- * and die if it is a version we don't understand. Generally one would\n- * set_git_dir() before calling this, and use it only for \"are we in a valid\n- * repo?\".\n- *\n- * If successful and fmt is not NULL, fill fmt with data.\n- */\n-static void check_and_apply_repository_format(struct repository *repo,\n-\t\t\t\t\t      struct repository_format *fmt,\n-\t\t\t\t\t      enum apply_repository_format_flags flags)\n-{\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\n-\tif (!fmt)\n-\t\tfmt = &repo_fmt;\n-\n-\tcheck_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);\n-\tif (apply_repository_format(repo, fmt, flags, &err) < 0)\n-\t\tdie(\"%s\", err.buf);\n-\tstartup_info->have_repository = 1;\n-\n-\tclear_repository_format(&repo_fmt);\n-}\n-\n const char *enter_repo(struct repository *repo, const char *path, unsigned flags)\n {\n \tstatic struct strbuf validated_path = STRBUF_INIT;\n@@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags\n \t}\n \n \tif (is_git_directory(\".\")) {\n+\t\tstruct repository_format fmt = REPOSITORY_FORMAT_INIT;\n+\t\tstruct strbuf err = STRBUF_INIT;\n+\n \t\tset_git_dir(repo, \".\", 0);\n-\t\tcheck_and_apply_repository_format(repo, NULL,\n-\t\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n+\t\tcheck_repository_format_gently(\".\", &fmt, NULL);\n+\t\tif (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\t\tdie(\"%s\", err.buf);\n+\t\tstartup_info->have_repository = 1;\n+\n+\t\tclear_repository_format(&fmt);\n+\t\tstrbuf_release(&err);\n \t\treturn path;\n \t}\n \n@@ -2820,6 +2802,7 @@ int init_db(struct repository *repo,\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n \n \tif (real_git_dir) {\n \t\tstruct stat st;\n@@ -2846,9 +2829,10 @@ int init_db(struct repository *repo,\n \t * config file, so this will not fail.  What we are catching\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n-\tcheck_and_apply_repository_format(repo, &repo_fmt,\n-\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n-\n+\tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\tdie(\"%s\", err.buf);\n+\tstartup_info->have_repository = 1;\n \trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n@@ -2904,6 +2888,7 @@ int init_db(struct repository *repo,\n \t}\n \n \tclear_repository_format(&repo_fmt);\n+\tstrbuf_release(&err);\n \tfree(original_git_dir);\n \treturn 0;\n }\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545576","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-2-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 2/8] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:48Z","receivedAt":"2026-06-15T13:56:59Z","isPatch":true,"body":"When discovering the repository in \"setup.c\" we apply the final\nrepository format multiple times:\n\n  - Once via `repository_format_configure()`, where we apply the hash\n    algorithm and ref storage format to both `struct repository_format`\n    and `struct repository`.\n\n  - And once via `apply_repository_format()`, where we apply these two\n    settings from `struct repository_format` to `struct repository`.\n\nWith the current flow both of these are in fact necessary. But this is\nonly because we call `repository_format_configure()` after we have\ncalled `apply_repository_format()`. Consequently, if we only changed the\nrepository format in `repository_format_configure()` it would never\npropagate to the repository.\n\nRefactor the code so that we first configure the repository format\nbefore applying it to the repository so that we can stop setting the\nhash and reference storage format multiple times.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 9 ++-------\n 1 file changed, 2 insertions(+), 7 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a9db1f2c23..2748155964 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n \treturn ret;\n }\n \n-static void repository_format_configure(struct repository *repo,\n-\t\t\t\t\tstruct repository_format *repo_fmt,\n+static void repository_format_configure(struct repository_format *repo_fmt,\n \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n {\n \tstruct default_format_config cfg = {\n@@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n \t\trepo_fmt->hash_algo = cfg.hash;\n \t}\n-\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n \n \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n \tif (repo_fmt->version >= 0 &&\n@@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n \n \t\tfree(backend);\n \t}\n-\n-\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n-\t\t\t\t    repo_fmt->ref_storage_payload);\n }\n \n int init_db(struct repository *repo,\n@@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n \t\tdie(\"%s\", err.buf);\n \tstartup_info->have_repository = 1;\n-\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545577","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-3-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 3/8] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:49Z","receivedAt":"2026-06-15T13:57:01Z","isPatch":true,"body":"When discovering a repository we eventually also apply the\n\"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\ntwo problems with that:\n\n  - We do this unconditionally, which is rather pointless: we really\n    only have to configure the repository when we have found one.\n\n  - We have already applied the repository format at that point in time,\n    so we need to manually reapply it.\n\nMove the logic around so that we only apply the environment variable\nwhen a repository was discovered. This also allows us to drop the\nexplcit call to `repo_set_ref_storage_format()` because we now adjust\nthe format before we apply it via `apply_repository_format()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 40 ++++++++++++++++++++--------------------\n 1 file changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 2748155964..7b2e50a8c5 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \tstatic struct strbuf cwd = STRBUF_INIT;\n \tstruct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;\n \tconst char *prefix = NULL;\n-\tconst char *ref_backend_uri;\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n \n \t/*\n@@ -2023,6 +2022,8 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t    startup_info->have_repository ||\n \t    /* GIT_DIR_EXPLICIT */\n \t    getenv(GIT_DIR_ENVIRONMENT)) {\n+\t\tconst char *ref_backend_uri;\n+\n \t\tif (!repo->gitdir) {\n \t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n \t\t\tif (!gitdir)\n@@ -2030,6 +2031,24 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\t\tsetup_git_env_internal(repo, gitdir);\n \t\t}\n \n+\t\t/*\n+\t\t * The env variable should override the repository config\n+\t\t * for 'extensions.refStorage'.\n+\t\t */\n+\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n+\t\tif (ref_backend_uri) {\n+\t\t\tchar *format;\n+\n+\t\t\tfree(repo_fmt.ref_storage_payload);\n+\n+\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n+\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n+\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n+\n+\t\t\tfree(format);\n+\t\t}\n+\n \t\tif (startup_info->have_repository) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n \n@@ -2057,25 +2076,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n-\t/*\n-\t * The env variable should override the repository config\n-\t * for 'extensions.refStorage'.\n-\t */\n-\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n-\tif (ref_backend_uri) {\n-\t\tchar *backend, *payload;\n-\t\tenum ref_storage_format format;\n-\n-\t\tparse_reference_uri(ref_backend_uri, &backend, &payload);\n-\t\tformat = ref_storage_format_by_name(backend);\n-\t\tif (format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\t\tdie(_(\"unknown ref storage format: '%s'\"), backend);\n-\t\trepo_set_ref_storage_format(repo, format, payload);\n-\n-\t\tfree(backend);\n-\t\tfree(payload);\n-\t}\n-\n \tsetup_original_cwd(repo);\n \n \tstrbuf_release(&dir);\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545578","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-4-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 4/8] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:50Z","receivedAt":"2026-06-15T13:57:04Z","isPatch":true,"body":"When creating reference stores we register them with the \"chdir_notify\"\nsubsystem. This is required because some of the paths we track may be\nrelative paths, so we have to reparent them in case the current working\ndirectory changes.\n\nBut while we register the reference stores, we never unregister them.\nThis can have multiple outcomes:\n\n  - For a repository's main reference database we essentially keep the\n    pointer alive. We never free that database, either, and our leak\n    checker doesn't notice because it's still registered.\n\n  - For submodule and worktree reference databases we do eventually free\n    them in `repo_clear()`, so we may keep pointers to free'd memory\n    registered. We never notice though as we don't tend to chdir around\n    in the middle of the process.\n\nWe never noticed either of these symptoms, but they are obviously bad.\n\nPartially fix those issues by unregistering the reference stores when\nreleasing them. The leak of the main reference database will be fixed in\na subsequent commit.\n\nNote that this requires us to use `chdir_notify_register()` instead of\n`chdir_notify_reparent()`, as there is no infrastructure to unregister the\nlatter. It ultimately doesn't matter much though: in a subsequent commit\nwe'll drop this infrastructure completely. We merely require this step\nhere so that we can fix the memory leaks ahead of time.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c    | 22 +++++++++++++++++++---\n refs/packed-backend.c   | 16 +++++++++++++++-\n refs/reftable-backend.c | 16 +++++++++++++++-\n 3 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex a4c7858787..296981584b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n+static void files_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t     const char *old_cwd,\n+\t\t\t\t     const char *new_cwd,\n+\t\t\t\t     void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n+\tfree(refs->gitcommondir);\n+\trefs->gitcommondir = tmp;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n-\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n-\t\t\t      &refs->gitcommondir);\n+\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n \n@@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n+\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 0acde48c45..499cb55dfa 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n+static void packed_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *payload)\n+{\n+\tstruct packed_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n+\tfree(refs->path);\n+\trefs->path = tmp;\n+}\n+\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n+\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n \treturn ref_store;\n }\n \n@@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n+\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4ae22922de..8c93070677 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static void reftable_be_reparent(const char *name UNUSED,\n+\t\t\t\t const char *old_cwd,\n+\t\t\t\t const char *new_cwd,\n+\t\t\t\t void *payload)\n+{\n+\tstruct reftable_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+}\n+\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n+\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n \n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n@@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n+\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545579","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-5-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 5/8] chdir-notify: drop unused `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:51Z","receivedAt":"2026-06-15T13:57:07Z","isPatch":true,"body":"With the preceding commit we've removed all callers of\n`chdir_notify_reparent()`, so the function is unused now. Drop it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n chdir-notify.c | 26 --------------------------\n chdir-notify.h |  6 +-----\n 2 files changed, 1 insertion(+), 31 deletions(-)\n\ndiff --git a/chdir-notify.c b/chdir-notify.c\nindex f8bfe3cbef..1237a45e2e 100644\n--- a/chdir-notify.c\n+++ b/chdir-notify.c\n@@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t}\n }\n \n-static void reparent_cb(const char *name,\n-\t\t\tconst char *old_cwd,\n-\t\t\tconst char *new_cwd,\n-\t\t\tvoid *data)\n-{\n-\tchar **path = data;\n-\tchar *tmp = *path;\n-\n-\tif (!tmp)\n-\t\treturn;\n-\n-\t*path = reparent_relative_path(old_cwd, new_cwd, tmp);\n-\tfree(tmp);\n-\n-\tif (name) {\n-\t\ttrace_printf_key(&trace_setup_key,\n-\t\t\t\t \"setup: reparent %s to '%s'\",\n-\t\t\t\t name, *path);\n-\t}\n-}\n-\n-void chdir_notify_reparent(const char *name, char **path)\n-{\n-\tchdir_notify_register(name, reparent_cb, path);\n-}\n-\n int chdir_notify(const char *new_cwd)\n {\n \tstruct strbuf old_cwd = STRBUF_INIT;\ndiff --git a/chdir-notify.h b/chdir-notify.h\nindex 81eb69d846..36b4114472 100644\n--- a/chdir-notify.h\n+++ b/chdir-notify.h\n@@ -19,10 +19,7 @@\n  *   chdir_notify_register(\"description\", foo, data);\n  *\n  * In practice most callers will want to move a relative path to the new root;\n- * they can use the reparent_relative_path() helper for that. If that's all\n- * you're doing, you can also use the convenience function:\n- *\n- *   chdir_notify_reparent(\"description\", &my_path);\n+ * they can use the reparent_relative_path() helper for that.\n  *\n  * Whenever a chdir event occurs, that will update my_path (if it's relative)\n  * to adjust for the new cwd by freeing any existing string and allocating a\n@@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name,\n void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data);\n void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t\t\t     void *data);\n-void chdir_notify_reparent(const char *name, char **path);\n \n /*\n  *\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545580","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-6-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 6/8] repository: free main reference database","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:52Z","receivedAt":"2026-06-15T13:57:09Z","isPatch":true,"body":"While we release worktree and submodule reference databases when\nclearing a repository, we don't ever release the main reference\ndatabase. This memory leak went unnoticed because its pointer is\nkept alive by the \"chdir_notify\" subsystem.\n\nFix the memory leak.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex 187dd471c4..e2b5c6712b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->refs_private) {\n+\t\tref_store_release(repo->refs_private);\n+\t\tFREE_AND_NULL(repo->refs_private);\n+\t}\n+\n \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n \t\tref_store_release(e->value);\n \tstrmap_clear(&repo->submodule_ref_stores, 1);\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545581","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-7-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:53Z","receivedAt":"2026-06-15T13:57:12Z","isPatch":true,"body":"When we have an \"onbranch\" condition we need to ask the reference\ndatabase whether HEAD currently points at the configured branch. This\nunfortunately creates a chicken-and-egg problem:\n\n  - The reference database needs to read the configuration so that it\n    can configure itself.\n\n  - The configuration needs to construct a reference database to fully\n    parse all of its conditionals.\n\nThe way we handle this is by simply excluding \"onbranch\" conditionals\nwhen we haven't yet configured the reference database.\n\nThe mechanism for this is broken though: to verify whether or not we\nhave configured the reference database we check whether its format is\nset to `REF_STORAGE_UNKNOWN` in `include_by_branch()`. But typically,\nthe format _is_ already known at that time because we set it up during\nrepository discovery in \"setup.c\".\n\nThe consequence is that we have recursion:\n\n  1. We call `get_main_ref_store()`.\n\n  2. We don't yet have a reference store, so we call `ref_store_init()`.\n\n  3. We parse the configuration required for the reference store.\n\n  4. We eventually end up in `include_by_branch()`.\n\n  5. We have already configured the reference storage format, so we end\n     up calling `get_main_ref_store()` again.\n\nWe still haven't finished (1) though, so `get_main_ref_store()` will now\ncall `ref_store_init()` a second time. The end result is that we have\nconstructed the same reference store twice.\n\nOf course, as both reference stores would be assigned to `refs_private`,\nwe leak one of those two instances. This never surfaced as an actual\nleak though because the pointer is kept alive by the \"chdir_notify\"\nsubsystem.\n\nFor now, we can fix the issue by explicitly unsetting the reference\nstorage format before constructing it. This makes the mentioned check\ntrigger as expected, and consequently we won't end up constructing a\nsecond reference database at all. Ultimately, this means that we\nconsistently stop evaluating \"onbranch\" conditions when constructing the\nmain reference database.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 20 ++++++++++++++++++--\n 1 file changed, 18 insertions(+), 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex d3caa9a633..e69b9b8ac8 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2351,15 +2351,31 @@ void ref_store_release(struct ref_store *ref_store)\n \n struct ref_store *get_main_ref_store(struct repository *r)\n {\n+\tenum ref_storage_format format;\n+\n \tif (r->refs_private)\n \t\treturn r->refs_private;\n \n \tif (!r->gitdir)\n \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n \n-\tr->refs_private = ref_store_init(r, r->ref_storage_format,\n-\t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n+\t/*\n+\t * When constructing the reference backend we'll end up reading the Git\n+\t * configuration. This means we'll also try to evaluate \"onbranch\"\n+\t * conditions.\n+\t *\n+\t * We cannot read branches when constructing the refdb, so it is not\n+\t * possible to evaluate those conditions in the first place. To gate\n+\t * their evaluation we check whether or not the reference storage\n+\t * format has been configured -- we thus have to temporarily set it to\n+\t * UNKNOWN here so that we don't end up recursing.\n+\t */\n+\tformat = r->ref_storage_format;\n+\tr->ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n+\tr->refs_private = ref_store_init(r, format, r->gitdir, REF_STORE_ALL_CAPS);\n \tr->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);\n+\tr->ref_storage_format = format;\n+\n \treturn r->refs_private;\n }\n \n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545582","messageId":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-8-f4854aa99859@pks.im","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im","subject":"[PATCH v2 8/8] refs: drop local buffer in `refs_compute_filesystem_location()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-15T13:56:54Z","receivedAt":"2026-06-15T13:57:14Z","isPatch":true,"body":"We're using a local buffer in `refs_compute_filesystem_location()` that\nis only used so that we can fill it and then call `strbuf_realpath()` on\nits result. This roundtrip isn't necessary though: `strbuf_realpath()`\nalready knows to use a single buffer as both input and output at the\nsame time. So all this does is to add a bit of confusion and an extra\nmemory allocation.\n\nDrop the local buffer.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 8 ++------\n 1 file changed, 2 insertions(+), 6 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex e69b9b8ac8..4912510590 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -3571,8 +3571,6 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\t\t      bool *is_worktree, struct strbuf *refdir,\n \t\t\t\t      struct strbuf *ref_common_dir)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n-\n \t*is_worktree = get_common_dir_noenv(ref_common_dir, gitdir);\n \n \tif (!payload) {\n@@ -3586,8 +3584,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t}\n \n \tif (!is_absolute_path(payload)) {\n-\t\tstrbuf_addf(&sb, \"%s/%s\", ref_common_dir->buf, payload);\n-\t\tstrbuf_realpath(ref_common_dir, sb.buf, 1);\n+\t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n+\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n \t} else {\n \t\tstrbuf_realpath(ref_common_dir, payload, 1);\n \t}\n@@ -3600,6 +3598,4 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\tBUG(\"worktree path does not contain slash\");\n \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n \t}\n-\n-\tstrbuf_release(&sb);\n }\n\n-- \n2.55.0.rc0.738.g0c8ab3ebcc.dirty\n\n"},{"id":"545783","messageId":"ajLV1if5XYO-pyNb@denethor","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-2-f4854aa99859@pks.im","subject":"Re: [PATCH v2 2/8] setup: stop applying repository format twice","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T17:22:50Z","receivedAt":"2026-06-17T17:22:55Z","isPatch":true,"body":"On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> When discovering the repository in \"setup.c\" we apply the final\n> repository format multiple times:\n> \n>   - Once via `repository_format_configure()`, where we apply the hash\n>     algorithm and ref storage format to both `struct repository_format`\n>     and `struct repository`.\n> \n>   - And once via `apply_repository_format()`, where we apply these two\n>     settings from `struct repository_format` to `struct repository`.\n> \n> With the current flow both of these are in fact necessary. But this is\n> only because we call `repository_format_configure()` after we have\n> called `apply_repository_format()`. Consequently, if we only changed the\n> repository format in `repository_format_configure()` it would never\n> propagate to the repository.\n\nOk, so because `repository_format_configure()` is invoked after the\nrepository format was already applied, it had to explictly configure the\nrepository as well.\n\n> Refactor the code so that we first configure the repository format\n> before applying it to the repository so that we can stop setting the\n> hash and reference storage format multiple times.\n\nMakes sense. Sounds like a good change.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  setup.c | 9 ++-------\n>  1 file changed, 2 insertions(+), 7 deletions(-)\n> \n> diff --git a/setup.c b/setup.c\n> index a9db1f2c23..2748155964 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n>  \treturn ret;\n>  }\n>  \n> -static void repository_format_configure(struct repository *repo,\n> -\t\t\t\t\tstruct repository_format *repo_fmt,\n> +static void repository_format_configure(struct repository_format *repo_fmt,\n>  \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n\nWe now only care about configuring the repository format and will let\n`apply_repository_format()` handle setting the repository. Looks good.\n\n[snip]\n> @@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n>  \t * is an attempt to reinitialize new repository with an old tool.\n>  \t */\n>  \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n> +\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n>  \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n>  \t\tdie(\"%s\", err.buf);\n>  \tstartup_info->have_repository = 1;\n> -\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n\n`apply_repository_format()` already has the logic to set the hash algo\nand ref storage format from the repository format, so change changing\nthe order here is ok and a good change.\n\n-Justin\n"},{"id":"545784","messageId":"ajLapsLze_zF-dsS@denethor","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-3-f4854aa99859@pks.im","subject":"Re: [PATCH v2 3/8] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T17:43:02Z","receivedAt":"2026-06-17T17:43:06Z","isPatch":true,"body":"On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> When discovering a repository we eventually also apply the\n> \"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\n> two problems with that:\n> \n>   - We do this unconditionally, which is rather pointless: we really\n>     only have to configure the repository when we have found one.\n\nI agree that configuring the repository reference format when there\nisn't a repository to begin doesn't sound very useful.\n\n>   - We have already applied the repository format at that point in time,\n>     so we need to manually reapply it.\n> \n> Move the logic around so that we only apply the environment variable\n> when a repository was discovered. This also allows us to drop the\n> explcit call to `repo_set_ref_storage_format()` because we now adjust\n> the format before we apply it via `apply_repository_format()`.\n\nMake sense.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n[snip]\n> @@ -2023,6 +2022,8 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n>  \t    startup_info->have_repository ||\n>  \t    /* GIT_DIR_EXPLICIT */\n>  \t    getenv(GIT_DIR_ENVIRONMENT)) {\n> +\t\tconst char *ref_backend_uri;\n> +\n>  \t\tif (!repo->gitdir) {\n>  \t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n>  \t\t\tif (!gitdir)\n> @@ -2030,6 +2031,24 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n>  \t\t\tsetup_git_env_internal(repo, gitdir);\n>  \t\t}\n>  \n> +\t\t/*\n> +\t\t * The env variable should override the repository config\n> +\t\t * for 'extensions.refStorage'.\n> +\t\t */\n> +\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n> +\t\tif (ref_backend_uri) {\n> +\t\t\tchar *format;\n> +\n> +\t\t\tfree(repo_fmt.ref_storage_payload);\n> +\n> +\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n> +\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n> +\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n> +\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n> +\n> +\t\t\tfree(format);\n> +\t\t}\n> +\n>  \t\tif (startup_info->have_repository) {\n>  \t\t\tstruct strbuf err = STRBUF_INIT;\n\nHmmm, we only invoke `apply_repository_format()` if we indeed have a\nrepository (having just GIT_DIR_ENVIRONMENT set isn't enough). Should we\ninstead nest this logic right above `apply_repository_format()` in the\nsame block?\n\n-Justin\n"},{"id":"545786","messageId":"ajLdIY_fxkKDTBaW@denethor","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-4-f4854aa99859@pks.im","subject":"Re: [PATCH v2 4/8] refs: unregister reference stores from \"chdir_notify\"","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T18:02:23Z","receivedAt":"2026-06-17T18:02:28Z","isPatch":true,"body":"On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> When creating reference stores we register them with the \"chdir_notify\"\n> subsystem. This is required because some of the paths we track may be\n> relative paths, so we have to reparent them in case the current working\n> directory changes.\n> \n> But while we register the reference stores, we never unregister them.\n> This can have multiple outcomes:\n> \n>   - For a repository's main reference database we essentially keep the\n>     pointer alive. We never free that database, either, and our leak\n>     checker doesn't notice because it's still registered.\n> \n>   - For submodule and worktree reference databases we do eventually free\n>     them in `repo_clear()`, so we may keep pointers to free'd memory\n>     registered. We never notice though as we don't tend to chdir around\n>     in the middle of the process.\n> \n> We never noticed either of these symptoms, but they are obviously bad.\n> \n> Partially fix those issues by unregistering the reference stores when\n> releasing them. The leak of the main reference database will be fixed in\n> a subsequent commit.\n> \n> Note that this requires us to use `chdir_notify_register()` instead of\n> `chdir_notify_reparent()`, as there is no infrastructure to unregister the\n> latter. It ultimately doesn't matter much though: in a subsequent commit\n> we'll drop this infrastructure completely. We merely require this step\n> here so that we can fix the memory leaks ahead of time.\n\nSince this version of the series dropped the last patch which stopped\nusing `chdir_notify_reparent()`, does the log message here need to be\nupdated?\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/files-backend.c    | 22 +++++++++++++++++++---\n>  refs/packed-backend.c   | 16 +++++++++++++++-\n>  refs/reftable-backend.c | 16 +++++++++++++++-\n>  3 files changed, 49 insertions(+), 5 deletions(-)\n> \n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index a4c7858787..296981584b 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n>  \t}\n>  }\n>  \n> +static void files_ref_store_reparent(const char *name UNUSED,\n> +\t\t\t\t     const char *old_cwd,\n> +\t\t\t\t     const char *new_cwd,\n> +\t\t\t\t     void *payload)\n> +{\n> +\tstruct files_ref_store *refs = payload;\n> +\tchar *tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n> +\tfree(refs->base.gitdir);\n> +\trefs->base.gitdir = tmp;\n> +\n> +\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n> +\tfree(refs->gitcommondir);\n> +\trefs->gitcommondir = tmp;\n> +}\n\nOk, here is introduce a callback specific to the file ref store to\nhandle reparenting both the gitdir and commondir.\n\n>  /*\n>   * Create a new submodule ref cache and add it to the internal\n>   * set of caches.\n> @@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n>  \n>  \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n>  \n> -\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n> -\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n> -\t\t\t      &refs->gitcommondir);\n> +\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n\nWe use the new callback here instead of relying on the generic callback\nused by `chdir_notify_reparent()`.\n\n>  \tstrbuf_release(&refdir);\n>  \n> @@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n>  \tfree(refs->gitcommondir);\n>  \tref_store_release(refs->packed_ref_store);\n>  \tfree(refs->packed_ref_store);\n> +\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n\nThis allows us to unregister the callback and avoid holding on\nreferences which may have been free'd. Makes sense.\n\nThe rest of the patch does the exact same for the packed ref store and\nreftable BE which look correct too.\n\n-Justin\n"},{"id":"545787","messageId":"ajLhlWqkJLqCzp7v@denethor","threadId":"65786","inReplyTo":"ajLdIY_fxkKDTBaW@denethor","subject":"Re: [PATCH v2 4/8] refs: unregister reference stores from \"chdir_notify\"","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T18:07:42Z","receivedAt":"2026-06-17T18:07:46Z","isPatch":true,"body":"On 26/06/17 01:02PM, Justin Tobler wrote:\n> On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> > Note that this requires us to use `chdir_notify_register()` instead of\n> > `chdir_notify_reparent()`, as there is no infrastructure to unregister the\n> > latter. It ultimately doesn't matter much though: in a subsequent commit\n> > we'll drop this infrastructure completely. We merely require this step\n> > here so that we can fix the memory leaks ahead of time.\n> \n> Since this version of the series dropped the last patch which stopped\n> using `chdir_notify_reparent()`, does the log message here need to be\n> updated?\n\nAfter looking at the next patch, I realized we are referring to just the\n`chdir_notify_reparent()` function here which is no longer used. The\ncurrent log message makes sense.\n\n-Justin\n"},{"id":"545788","messageId":"ajLixYJTl3c-lSn3@denethor","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-6-f4854aa99859@pks.im","subject":"Re: [PATCH v2 6/8] repository: free main reference database","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T18:09:24Z","receivedAt":"2026-06-17T18:09:26Z","isPatch":true,"body":"On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> While we release worktree and submodule reference databases when\n> clearing a repository, we don't ever release the main reference\n> database. This memory leak went unnoticed because its pointer is\n> kept alive by the \"chdir_notify\" subsystem.\n> \n> Fix the memory leak.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  repository.c | 5 +++++\n>  1 file changed, 5 insertions(+)\n> \n> diff --git a/repository.c b/repository.c\n> index 187dd471c4..e2b5c6712b 100644\n> --- a/repository.c\n> +++ b/repository.c\n> @@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n>  \t\tFREE_AND_NULL(repo->remote_state);\n>  \t}\n>  \n> +\tif (repo->refs_private) {\n> +\t\tref_store_release(repo->refs_private);\n> +\t\tFREE_AND_NULL(repo->refs_private);\n> +\t}\n\nNice fix. :)\n"},{"id":"545789","messageId":"ajLoiCS2mXP49eAJ@denethor","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-7-f4854aa99859@pks.im","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-17T18:41:40Z","receivedAt":"2026-06-17T18:41:45Z","isPatch":true,"body":"On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n[snip]\n> diff --git a/refs.c b/refs.c\n> index d3caa9a633..e69b9b8ac8 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2351,15 +2351,31 @@ void ref_store_release(struct ref_store *ref_store)\n>  \n>  struct ref_store *get_main_ref_store(struct repository *r)\n>  {\n> +\tenum ref_storage_format format;\n> +\n>  \tif (r->refs_private)\n>  \t\treturn r->refs_private;\n>  \n>  \tif (!r->gitdir)\n>  \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n>  \n> -\tr->refs_private = ref_store_init(r, r->ref_storage_format,\n> -\t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n> +\t/*\n> +\t * When constructing the reference backend we'll end up reading the Git\n> +\t * configuration. This means we'll also try to evaluate \"onbranch\"\n> +\t * conditions.\n> +\t *\n> +\t * We cannot read branches when constructing the refdb, so it is not\n> +\t * possible to evaluate those conditions in the first place. To gate\n> +\t * their evaluation we check whether or not the reference storage\n> +\t * format has been configured -- we thus have to temporarily set it to\n> +\t * UNKNOWN here so that we don't end up recursing.\n> +\t */\n> +\tformat = r->ref_storage_format;\n> +\tr->ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n\nIs this really the best signal to indicate that a repository ref store\nhas not been initialized? Temporarily setting the storage format to\nREF_STORAGE_FORMAT_UNKNOWN feels rather awkward and suggests to me that\n`include_by_branch()` probably shouldn't be using it to begin with if\nits not reliable.\n\n-Justin\n"},{"id":"545813","messageId":"ajOJM8EvGWWkYNuL@pks.im","threadId":"65786","inReplyTo":"ajLoiCS2mXP49eAJ@denethor","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T05:59:15Z","receivedAt":"2026-06-18T05:59:22Z","isPatch":true,"body":"On Wed, Jun 17, 2026 at 01:41:40PM -0500, Justin Tobler wrote:\n> On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> [snip]\n> > diff --git a/refs.c b/refs.c\n> > index d3caa9a633..e69b9b8ac8 100644\n> > --- a/refs.c\n> > +++ b/refs.c\n> > @@ -2351,15 +2351,31 @@ void ref_store_release(struct ref_store *ref_store)\n> >  \n> >  struct ref_store *get_main_ref_store(struct repository *r)\n> >  {\n> > +\tenum ref_storage_format format;\n> > +\n> >  \tif (r->refs_private)\n> >  \t\treturn r->refs_private;\n> >  \n> >  \tif (!r->gitdir)\n> >  \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n> >  \n> > -\tr->refs_private = ref_store_init(r, r->ref_storage_format,\n> > -\t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n> > +\t/*\n> > +\t * When constructing the reference backend we'll end up reading the Git\n> > +\t * configuration. This means we'll also try to evaluate \"onbranch\"\n> > +\t * conditions.\n> > +\t *\n> > +\t * We cannot read branches when constructing the refdb, so it is not\n> > +\t * possible to evaluate those conditions in the first place. To gate\n> > +\t * their evaluation we check whether or not the reference storage\n> > +\t * format has been configured -- we thus have to temporarily set it to\n> > +\t * UNKNOWN here so that we don't end up recursing.\n> > +\t */\n> > +\tformat = r->ref_storage_format;\n> > +\tr->ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n> \n> Is this really the best signal to indicate that a repository ref store\n> has not been initialized? Temporarily setting the storage format to\n> REF_STORAGE_FORMAT_UNKNOWN feels rather awkward and suggests to me that\n> `include_by_branch()` probably shouldn't be using it to begin with if\n> its not reliable.\n\nTrue, but we don't really have a better signal to the best of my\nknowledge. Ideally, we'd be able to use the existence `r->refs_private`\nas signal. But that doesn't really work as the reference database is\nlazily constructed, and the recursion happens in the exact function that\nwould construct it in the first place. And there indeed are cases where\nreading the configuration is the first caller of `get_main_ref_store()`.\n\nMy first internal iteration tried to make this non-lazily constructed so\nthat we can use it as a proper signal. But that led to a bunch of\nproblems where we now parsed configuration way earlier than we currently\ndo, and that in turn led to all kinds of errors. I was able to fix all\nof those errors except one: we expect `git config set` to work in a\nmisconfigured repository so that the user can fix the misconfig without\nhaving to manually edit the Git configuration files. But when\nconstructing the refdb eagerly we will die early in such cases.\n\nWe could again work around that issue, but that unfortunately evolved\ninto a proper mess that I eventually discarded as unworkable. I think\nthis is an inherent design flaw: constructing the refdb requires us to\nbe able to parse the configuration, but constructing the configuration\nmay require us to construct the refdb. So this awkwardness is built into\nGit's design, unfortunately.\n\nSo I'd really love to have a better signal, as I fully agree that the\nabove workaround is nothing more but a hack. But I'm just not sure what\nthat signal would be. And this version here does exactly what we want:\nwe honor \"onbranch\" conditionals in all cases, except when constructing\nthe main reference store. Even if it's ugly.\n\nPatrick\n"},{"id":"545817","messageId":"ajOWBVqNVlS7grtG@pks.im","threadId":"65786","inReplyTo":"ajLapsLze_zF-dsS@denethor","subject":"Re: [PATCH v2 3/8] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:53:57Z","receivedAt":"2026-06-18T06:54:08Z","isPatch":true,"body":"On Wed, Jun 17, 2026 at 12:43:02PM -0500, Justin Tobler wrote:\n> On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> > @@ -2030,6 +2031,24 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n> >  \t\t\tsetup_git_env_internal(repo, gitdir);\n> >  \t\t}\n> >  \n> > +\t\t/*\n> > +\t\t * The env variable should override the repository config\n> > +\t\t * for 'extensions.refStorage'.\n> > +\t\t */\n> > +\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n> > +\t\tif (ref_backend_uri) {\n> > +\t\t\tchar *format;\n> > +\n> > +\t\t\tfree(repo_fmt.ref_storage_payload);\n> > +\n> > +\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n> > +\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n> > +\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n> > +\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n> > +\n> > +\t\t\tfree(format);\n> > +\t\t}\n> > +\n> >  \t\tif (startup_info->have_repository) {\n> >  \t\t\tstruct strbuf err = STRBUF_INIT;\n> \n> Hmmm, we only invoke `apply_repository_format()` if we indeed have a\n> repository (having just GIT_DIR_ENVIRONMENT set isn't enough). Should we\n> instead nest this logic right above `apply_repository_format()` in the\n> same block?\n\nYup, that makes sense indeed.\n\nPatrick\n"},{"id":"545818","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH v3 0/8] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:29Z","receivedAt":"2026-06-18T06:54:40Z","isPatch":true,"body":"Hi,\n\nthis patch series is a follow-up of the discussion at [1]. It converts\nthe reference backends to always use absolute paths internally, which\nthen allows us to drop the calls to `chdir_notify_reparent()`.\n\nUnfortunately, the series has grown quite a bit larger than anticipated.\nThis is due to a couple of weirdnesses in how the reference database is\nconstructed with an \"onbranch\" condition. We essentially construct the\nrefdb twice and loose one, but we never noticed because the chdir\nnotification subsystem kept the pointer to it reachable.\n\nNote that the first couple patches that touch \"setup.c\" aren't strictly\nrequired. They are a remnant of a previous iteration where I tried to\nsolve the issue in a different way. But I ultimately figured that these\nchanges are worth it by themselves as they simplify \"setup.c\" a bit.\n\nThis series is built on top of 1ff279f340 (The 13th batch, 2026-06-09)\nwith ps/setup-centralize-odb-creation at 42b9d3dc9d (setup: construct\nobject database in `apply_repository_format()`, 2026-06-04) merged into\nit.\n\nChanges in v3:\n  - Reduce the scope of applying the GIT_REFERENCE_BACKEND environment\n    variable even further so that we really only do this when we end up\n    applying the reference format.\n  - Fix a commit message that still referred to the dropped last commit.\n  - Link to v2: https://patch.msgid.link/20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im\n\nChanges in v2:\n  - Drop the last patch. This seemingly destroys the whole purpose of\n    the patch series, but after Peff's hint that this is actually a\n    performance optimization I'm less inclined to drop the chdir_notify\n    infra. I still think that the remainder of the patches make sense\n    standalone, as they simplify \"setup.c\" and clean memory leaks. Going\n    forward I'd like to investigate the idea of introducing a `struct\n    fsroot` infrastructure that uses the platform-equivalent of openat\n    et al.\n  - Improve a couple of commit messages.\n  - Link to v1: https://patch.msgid.link/20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <aifAVpxanV31KUpC@pks.im>\n\n---\nPatrick Steinhardt (8):\n      setup: inline `check_and_apply_repository_format()`\n      setup: stop applying repository format twice\n      setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n      refs: unregister reference stores from \"chdir_notify\"\n      chdir-notify: drop unused `chdir_notify_reparent()`\n      repository: free main reference database\n      refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n      refs: drop local buffer in `refs_compute_filesystem_location()`\n\n chdir-notify.c          | 26 --------------\n chdir-notify.h          |  6 +---\n refs.c                  | 28 ++++++++++-----\n refs/files-backend.c    | 22 ++++++++++--\n refs/packed-backend.c   | 16 ++++++++-\n refs/reftable-backend.c | 16 ++++++++-\n repository.c            |  5 +++\n setup.c                 | 95 +++++++++++++++++++------------------------------\n 8 files changed, 112 insertions(+), 102 deletions(-)\n\nRange-diff versus v2:\n\n1:  ea89bedaa2 = 1:  2be38c1e02 setup: inline `check_and_apply_repository_format()`\n2:  b87f1db13b = 2:  7fdcd81bb7 setup: stop applying repository format twice\n3:  f72a8dc251 ! 3:  2162480668 setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n    @@ setup.c: const char *setup_git_directory_gently(struct repository *repo, int *no\n      \n      \t/*\n     @@ setup.c: const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n    - \t    startup_info->have_repository ||\n    - \t    /* GIT_DIR_EXPLICIT */\n    - \t    getenv(GIT_DIR_ENVIRONMENT)) {\n    -+\t\tconst char *ref_backend_uri;\n    -+\n    - \t\tif (!repo->gitdir) {\n    - \t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n    - \t\t\tif (!gitdir)\n    -@@ setup.c: const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n    - \t\t\tsetup_git_env_internal(repo, gitdir);\n    - \t\t}\n      \n    -+\t\t/*\n    -+\t\t * The env variable should override the repository config\n    -+\t\t * for 'extensions.refStorage'.\n    -+\t\t */\n    -+\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n    -+\t\tif (ref_backend_uri) {\n    -+\t\t\tchar *format;\n    + \t\tif (startup_info->have_repository) {\n    + \t\t\tstruct strbuf err = STRBUF_INIT;\n    ++\t\t\tconst char *ref_backend_uri;\n     +\n    -+\t\t\tfree(repo_fmt.ref_storage_payload);\n    ++\t\t\t/*\n    ++\t\t\t * The env variable should override the repository config\n    ++\t\t\t * for 'extensions.refStorage'.\n    ++\t\t\t */\n    ++\t\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n    ++\t\t\tif (ref_backend_uri) {\n    ++\t\t\t\tchar *format;\n     +\n    -+\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n    -+\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n    -+\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n    -+\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n    ++\t\t\t\tfree(repo_fmt.ref_storage_payload);\n     +\n    -+\t\t\tfree(format);\n    -+\t\t}\n    ++\t\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n    ++\t\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n    ++\t\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n    ++\t\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n     +\n    - \t\tif (startup_info->have_repository) {\n    - \t\t\tstruct strbuf err = STRBUF_INIT;\n    ++\t\t\t\tfree(format);\n    ++\t\t\t}\n      \n    + \t\t\tif (apply_repository_format(repo, &repo_fmt,\n    + \t\t\t\t\t\t    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n     @@ setup.c: const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n      \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n      \t}\n4:  17bdcdb4c5 ! 4:  14daa680b1 refs: unregister reference stores from \"chdir_notify\"\n    @@ Commit message\n     \n         Note that this requires us to use `chdir_notify_register()` instead of\n         `chdir_notify_reparent()`, as there is no infrastructure to unregister the\n    -    latter. It ultimately doesn't matter much though: in a subsequent commit\n    -    we'll drop this infrastructure completely. We merely require this step\n    -    here so that we can fix the memory leaks ahead of time.\n    +    latter.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n5:  c2f13a487e = 5:  89fe37ebe1 chdir-notify: drop unused `chdir_notify_reparent()`\n6:  730e4caeda = 6:  4a96b70db4 repository: free main reference database\n7:  1dda77cd19 = 7:  e48fc2d69d refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n8:  6d969bb023 = 8:  37935d50c8 refs: drop local buffer in `refs_compute_filesystem_location()`\n\n---\nbase-commit: 255322df35357168daefec8523a3cdc849edd6c1\nchange-id: 20260609-b4-pks-refs-avoid-chdir-notify-reparent-a4eaf1edbcab\n\n"},{"id":"545819","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-1-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 1/8] setup: inline `check_and_apply_repository_format()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:30Z","receivedAt":"2026-06-18T06:54:42Z","isPatch":true,"body":"We have two callsites of `check_and_apply_repository_format()`. In a\nsubsequent commit we'll want to adapt one of those callsites to change\nthe order in which we read and apply the repository format, at which\npoint the helper function will not really be a good fit for us anymore.\n\nInline the function to both of the callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 47 ++++++++++++++++-------------------------------\n 1 file changed, 16 insertions(+), 31 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex b4652651df..a9db1f2c23 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo,\n \treturn 0;\n }\n \n-/*\n- * Check the repository format version in the path found in repo_get_git_dir(repo),\n- * and die if it is a version we don't understand. Generally one would\n- * set_git_dir() before calling this, and use it only for \"are we in a valid\n- * repo?\".\n- *\n- * If successful and fmt is not NULL, fill fmt with data.\n- */\n-static void check_and_apply_repository_format(struct repository *repo,\n-\t\t\t\t\t      struct repository_format *fmt,\n-\t\t\t\t\t      enum apply_repository_format_flags flags)\n-{\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\n-\tif (!fmt)\n-\t\tfmt = &repo_fmt;\n-\n-\tcheck_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);\n-\tif (apply_repository_format(repo, fmt, flags, &err) < 0)\n-\t\tdie(\"%s\", err.buf);\n-\tstartup_info->have_repository = 1;\n-\n-\tclear_repository_format(&repo_fmt);\n-}\n-\n const char *enter_repo(struct repository *repo, const char *path, unsigned flags)\n {\n \tstatic struct strbuf validated_path = STRBUF_INIT;\n@@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags\n \t}\n \n \tif (is_git_directory(\".\")) {\n+\t\tstruct repository_format fmt = REPOSITORY_FORMAT_INIT;\n+\t\tstruct strbuf err = STRBUF_INIT;\n+\n \t\tset_git_dir(repo, \".\", 0);\n-\t\tcheck_and_apply_repository_format(repo, NULL,\n-\t\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n+\t\tcheck_repository_format_gently(\".\", &fmt, NULL);\n+\t\tif (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\t\tdie(\"%s\", err.buf);\n+\t\tstartup_info->have_repository = 1;\n+\n+\t\tclear_repository_format(&fmt);\n+\t\tstrbuf_release(&err);\n \t\treturn path;\n \t}\n \n@@ -2820,6 +2802,7 @@ int init_db(struct repository *repo,\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n \n \tif (real_git_dir) {\n \t\tstruct stat st;\n@@ -2846,9 +2829,10 @@ int init_db(struct repository *repo,\n \t * config file, so this will not fail.  What we are catching\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n-\tcheck_and_apply_repository_format(repo, &repo_fmt,\n-\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n-\n+\tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\tdie(\"%s\", err.buf);\n+\tstartup_info->have_repository = 1;\n \trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n@@ -2904,6 +2888,7 @@ int init_db(struct repository *repo,\n \t}\n \n \tclear_repository_format(&repo_fmt);\n+\tstrbuf_release(&err);\n \tfree(original_git_dir);\n \treturn 0;\n }\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545820","messageId":"ajOWLlNckbLfSC0t@pks.im","threadId":"65786","inReplyTo":"ajLdIY_fxkKDTBaW@denethor","subject":"Re: [PATCH v2 4/8] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:38Z","receivedAt":"2026-06-18T06:54:43Z","isPatch":true,"body":"On Wed, Jun 17, 2026 at 01:02:23PM -0500, Justin Tobler wrote:\n> On 26/06/15 03:56PM, Patrick Steinhardt wrote:\n> > When creating reference stores we register them with the \"chdir_notify\"\n> > subsystem. This is required because some of the paths we track may be\n> > relative paths, so we have to reparent them in case the current working\n> > directory changes.\n> > \n> > But while we register the reference stores, we never unregister them.\n> > This can have multiple outcomes:\n> > \n> >   - For a repository's main reference database we essentially keep the\n> >     pointer alive. We never free that database, either, and our leak\n> >     checker doesn't notice because it's still registered.\n> > \n> >   - For submodule and worktree reference databases we do eventually free\n> >     them in `repo_clear()`, so we may keep pointers to free'd memory\n> >     registered. We never notice though as we don't tend to chdir around\n> >     in the middle of the process.\n> > \n> > We never noticed either of these symptoms, but they are obviously bad.\n> > \n> > Partially fix those issues by unregistering the reference stores when\n> > releasing them. The leak of the main reference database will be fixed in\n> > a subsequent commit.\n> > \n> > Note that this requires us to use `chdir_notify_register()` instead of\n> > `chdir_notify_reparent()`, as there is no infrastructure to unregister the\n> > latter. It ultimately doesn't matter much though: in a subsequent commit\n> > we'll drop this infrastructure completely. We merely require this step\n> > here so that we can fix the memory leaks ahead of time.\n> \n> Since this version of the series dropped the last patch which stopped\n> using `chdir_notify_reparent()`, does the log message here need to be\n> updated?\n\nTrue, will do.\n\nPatrick\n"},{"id":"545821","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-2-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 2/8] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:31Z","receivedAt":"2026-06-18T06:54:45Z","isPatch":true,"body":"When discovering the repository in \"setup.c\" we apply the final\nrepository format multiple times:\n\n  - Once via `repository_format_configure()`, where we apply the hash\n    algorithm and ref storage format to both `struct repository_format`\n    and `struct repository`.\n\n  - And once via `apply_repository_format()`, where we apply these two\n    settings from `struct repository_format` to `struct repository`.\n\nWith the current flow both of these are in fact necessary. But this is\nonly because we call `repository_format_configure()` after we have\ncalled `apply_repository_format()`. Consequently, if we only changed the\nrepository format in `repository_format_configure()` it would never\npropagate to the repository.\n\nRefactor the code so that we first configure the repository format\nbefore applying it to the repository so that we can stop setting the\nhash and reference storage format multiple times.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 9 ++-------\n 1 file changed, 2 insertions(+), 7 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a9db1f2c23..2748155964 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n \treturn ret;\n }\n \n-static void repository_format_configure(struct repository *repo,\n-\t\t\t\t\tstruct repository_format *repo_fmt,\n+static void repository_format_configure(struct repository_format *repo_fmt,\n \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n {\n \tstruct default_format_config cfg = {\n@@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n \t\trepo_fmt->hash_algo = cfg.hash;\n \t}\n-\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n \n \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n \tif (repo_fmt->version >= 0 &&\n@@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n \n \t\tfree(backend);\n \t}\n-\n-\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n-\t\t\t\t    repo_fmt->ref_storage_payload);\n }\n \n int init_db(struct repository *repo,\n@@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n \t\tdie(\"%s\", err.buf);\n \tstartup_info->have_repository = 1;\n-\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545822","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-3-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 3/8] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:32Z","receivedAt":"2026-06-18T06:54:47Z","isPatch":true,"body":"When discovering a repository we eventually also apply the\n\"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\ntwo problems with that:\n\n  - We do this unconditionally, which is rather pointless: we really\n    only have to configure the repository when we have found one.\n\n  - We have already applied the repository format at that point in time,\n    so we need to manually reapply it.\n\nMove the logic around so that we only apply the environment variable\nwhen a repository was discovered. This also allows us to drop the\nexplcit call to `repo_set_ref_storage_format()` because we now adjust\nthe format before we apply it via `apply_repository_format()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 39 +++++++++++++++++++--------------------\n 1 file changed, 19 insertions(+), 20 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 2748155964..79125db565 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \tstatic struct strbuf cwd = STRBUF_INIT;\n \tstruct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;\n \tconst char *prefix = NULL;\n-\tconst char *ref_backend_uri;\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n \n \t/*\n@@ -2032,6 +2031,25 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \n \t\tif (startup_info->have_repository) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n+\t\t\tconst char *ref_backend_uri;\n+\n+\t\t\t/*\n+\t\t\t * The env variable should override the repository config\n+\t\t\t * for 'extensions.refStorage'.\n+\t\t\t */\n+\t\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n+\t\t\tif (ref_backend_uri) {\n+\t\t\t\tchar *format;\n+\n+\t\t\t\tfree(repo_fmt.ref_storage_payload);\n+\n+\t\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n+\t\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n+\t\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\t\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n+\n+\t\t\t\tfree(format);\n+\t\t\t}\n \n \t\t\tif (apply_repository_format(repo, &repo_fmt,\n \t\t\t\t\t\t    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n@@ -2057,25 +2075,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n-\t/*\n-\t * The env variable should override the repository config\n-\t * for 'extensions.refStorage'.\n-\t */\n-\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n-\tif (ref_backend_uri) {\n-\t\tchar *backend, *payload;\n-\t\tenum ref_storage_format format;\n-\n-\t\tparse_reference_uri(ref_backend_uri, &backend, &payload);\n-\t\tformat = ref_storage_format_by_name(backend);\n-\t\tif (format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\t\tdie(_(\"unknown ref storage format: '%s'\"), backend);\n-\t\trepo_set_ref_storage_format(repo, format, payload);\n-\n-\t\tfree(backend);\n-\t\tfree(payload);\n-\t}\n-\n \tsetup_original_cwd(repo);\n \n \tstrbuf_release(&dir);\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545823","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-4-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 4/8] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:33Z","receivedAt":"2026-06-18T06:54:49Z","isPatch":true,"body":"When creating reference stores we register them with the \"chdir_notify\"\nsubsystem. This is required because some of the paths we track may be\nrelative paths, so we have to reparent them in case the current working\ndirectory changes.\n\nBut while we register the reference stores, we never unregister them.\nThis can have multiple outcomes:\n\n  - For a repository's main reference database we essentially keep the\n    pointer alive. We never free that database, either, and our leak\n    checker doesn't notice because it's still registered.\n\n  - For submodule and worktree reference databases we do eventually free\n    them in `repo_clear()`, so we may keep pointers to free'd memory\n    registered. We never notice though as we don't tend to chdir around\n    in the middle of the process.\n\nWe never noticed either of these symptoms, but they are obviously bad.\n\nPartially fix those issues by unregistering the reference stores when\nreleasing them. The leak of the main reference database will be fixed in\na subsequent commit.\n\nNote that this requires us to use `chdir_notify_register()` instead of\n`chdir_notify_reparent()`, as there is no infrastructure to unregister the\nlatter.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c    | 22 +++++++++++++++++++---\n refs/packed-backend.c   | 16 +++++++++++++++-\n refs/reftable-backend.c | 16 +++++++++++++++-\n 3 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex a4c7858787..296981584b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n+static void files_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t     const char *old_cwd,\n+\t\t\t\t     const char *new_cwd,\n+\t\t\t\t     void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n+\tfree(refs->gitcommondir);\n+\trefs->gitcommondir = tmp;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n-\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n-\t\t\t      &refs->gitcommondir);\n+\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n \n@@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n+\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 0acde48c45..499cb55dfa 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n+static void packed_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *payload)\n+{\n+\tstruct packed_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n+\tfree(refs->path);\n+\trefs->path = tmp;\n+}\n+\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n+\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n \treturn ref_store;\n }\n \n@@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n+\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4ae22922de..8c93070677 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static void reftable_be_reparent(const char *name UNUSED,\n+\t\t\t\t const char *old_cwd,\n+\t\t\t\t const char *new_cwd,\n+\t\t\t\t void *payload)\n+{\n+\tstruct reftable_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+}\n+\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n+\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n \n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n@@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n+\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545824","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-5-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 5/8] chdir-notify: drop unused `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:34Z","receivedAt":"2026-06-18T06:54:52Z","isPatch":true,"body":"With the preceding commit we've removed all callers of\n`chdir_notify_reparent()`, so the function is unused now. Drop it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n chdir-notify.c | 26 --------------------------\n chdir-notify.h |  6 +-----\n 2 files changed, 1 insertion(+), 31 deletions(-)\n\ndiff --git a/chdir-notify.c b/chdir-notify.c\nindex f8bfe3cbef..1237a45e2e 100644\n--- a/chdir-notify.c\n+++ b/chdir-notify.c\n@@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t}\n }\n \n-static void reparent_cb(const char *name,\n-\t\t\tconst char *old_cwd,\n-\t\t\tconst char *new_cwd,\n-\t\t\tvoid *data)\n-{\n-\tchar **path = data;\n-\tchar *tmp = *path;\n-\n-\tif (!tmp)\n-\t\treturn;\n-\n-\t*path = reparent_relative_path(old_cwd, new_cwd, tmp);\n-\tfree(tmp);\n-\n-\tif (name) {\n-\t\ttrace_printf_key(&trace_setup_key,\n-\t\t\t\t \"setup: reparent %s to '%s'\",\n-\t\t\t\t name, *path);\n-\t}\n-}\n-\n-void chdir_notify_reparent(const char *name, char **path)\n-{\n-\tchdir_notify_register(name, reparent_cb, path);\n-}\n-\n int chdir_notify(const char *new_cwd)\n {\n \tstruct strbuf old_cwd = STRBUF_INIT;\ndiff --git a/chdir-notify.h b/chdir-notify.h\nindex 81eb69d846..36b4114472 100644\n--- a/chdir-notify.h\n+++ b/chdir-notify.h\n@@ -19,10 +19,7 @@\n  *   chdir_notify_register(\"description\", foo, data);\n  *\n  * In practice most callers will want to move a relative path to the new root;\n- * they can use the reparent_relative_path() helper for that. If that's all\n- * you're doing, you can also use the convenience function:\n- *\n- *   chdir_notify_reparent(\"description\", &my_path);\n+ * they can use the reparent_relative_path() helper for that.\n  *\n  * Whenever a chdir event occurs, that will update my_path (if it's relative)\n  * to adjust for the new cwd by freeing any existing string and allocating a\n@@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name,\n void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data);\n void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t\t\t     void *data);\n-void chdir_notify_reparent(const char *name, char **path);\n \n /*\n  *\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545825","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-6-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 6/8] repository: free main reference database","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:35Z","receivedAt":"2026-06-18T06:54:54Z","isPatch":true,"body":"While we release worktree and submodule reference databases when\nclearing a repository, we don't ever release the main reference\ndatabase. This memory leak went unnoticed because its pointer is\nkept alive by the \"chdir_notify\" subsystem.\n\nFix the memory leak.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex 187dd471c4..e2b5c6712b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->refs_private) {\n+\t\tref_store_release(repo->refs_private);\n+\t\tFREE_AND_NULL(repo->refs_private);\n+\t}\n+\n \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n \t\tref_store_release(e->value);\n \tstrmap_clear(&repo->submodule_ref_stores, 1);\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545826","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-7-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:36Z","receivedAt":"2026-06-18T06:54:57Z","isPatch":true,"body":"When we have an \"onbranch\" condition we need to ask the reference\ndatabase whether HEAD currently points at the configured branch. This\nunfortunately creates a chicken-and-egg problem:\n\n  - The reference database needs to read the configuration so that it\n    can configure itself.\n\n  - The configuration needs to construct a reference database to fully\n    parse all of its conditionals.\n\nThe way we handle this is by simply excluding \"onbranch\" conditionals\nwhen we haven't yet configured the reference database.\n\nThe mechanism for this is broken though: to verify whether or not we\nhave configured the reference database we check whether its format is\nset to `REF_STORAGE_UNKNOWN` in `include_by_branch()`. But typically,\nthe format _is_ already known at that time because we set it up during\nrepository discovery in \"setup.c\".\n\nThe consequence is that we have recursion:\n\n  1. We call `get_main_ref_store()`.\n\n  2. We don't yet have a reference store, so we call `ref_store_init()`.\n\n  3. We parse the configuration required for the reference store.\n\n  4. We eventually end up in `include_by_branch()`.\n\n  5. We have already configured the reference storage format, so we end\n     up calling `get_main_ref_store()` again.\n\nWe still haven't finished (1) though, so `get_main_ref_store()` will now\ncall `ref_store_init()` a second time. The end result is that we have\nconstructed the same reference store twice.\n\nOf course, as both reference stores would be assigned to `refs_private`,\nwe leak one of those two instances. This never surfaced as an actual\nleak though because the pointer is kept alive by the \"chdir_notify\"\nsubsystem.\n\nFor now, we can fix the issue by explicitly unsetting the reference\nstorage format before constructing it. This makes the mentioned check\ntrigger as expected, and consequently we won't end up constructing a\nsecond reference database at all. Ultimately, this means that we\nconsistently stop evaluating \"onbranch\" conditions when constructing the\nmain reference database.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 20 ++++++++++++++++++--\n 1 file changed, 18 insertions(+), 2 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex d3caa9a633..e69b9b8ac8 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2351,15 +2351,31 @@ void ref_store_release(struct ref_store *ref_store)\n \n struct ref_store *get_main_ref_store(struct repository *r)\n {\n+\tenum ref_storage_format format;\n+\n \tif (r->refs_private)\n \t\treturn r->refs_private;\n \n \tif (!r->gitdir)\n \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n \n-\tr->refs_private = ref_store_init(r, r->ref_storage_format,\n-\t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n+\t/*\n+\t * When constructing the reference backend we'll end up reading the Git\n+\t * configuration. This means we'll also try to evaluate \"onbranch\"\n+\t * conditions.\n+\t *\n+\t * We cannot read branches when constructing the refdb, so it is not\n+\t * possible to evaluate those conditions in the first place. To gate\n+\t * their evaluation we check whether or not the reference storage\n+\t * format has been configured -- we thus have to temporarily set it to\n+\t * UNKNOWN here so that we don't end up recursing.\n+\t */\n+\tformat = r->ref_storage_format;\n+\tr->ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;\n+\tr->refs_private = ref_store_init(r, format, r->gitdir, REF_STORE_ALL_CAPS);\n \tr->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);\n+\tr->ref_storage_format = format;\n+\n \treturn r->refs_private;\n }\n \n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545827","messageId":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-8-2a5669e8f486@pks.im","threadId":"65786","inReplyTo":"20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im","subject":"[PATCH v3 8/8] refs: drop local buffer in `refs_compute_filesystem_location()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T06:54:37Z","receivedAt":"2026-06-18T06:55:00Z","isPatch":true,"body":"We're using a local buffer in `refs_compute_filesystem_location()` that\nis only used so that we can fill it and then call `strbuf_realpath()` on\nits result. This roundtrip isn't necessary though: `strbuf_realpath()`\nalready knows to use a single buffer as both input and output at the\nsame time. So all this does is to add a bit of confusion and an extra\nmemory allocation.\n\nDrop the local buffer.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 8 ++------\n 1 file changed, 2 insertions(+), 6 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex e69b9b8ac8..4912510590 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -3571,8 +3571,6 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\t\t      bool *is_worktree, struct strbuf *refdir,\n \t\t\t\t      struct strbuf *ref_common_dir)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n-\n \t*is_worktree = get_common_dir_noenv(ref_common_dir, gitdir);\n \n \tif (!payload) {\n@@ -3586,8 +3584,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t}\n \n \tif (!is_absolute_path(payload)) {\n-\t\tstrbuf_addf(&sb, \"%s/%s\", ref_common_dir->buf, payload);\n-\t\tstrbuf_realpath(ref_common_dir, sb.buf, 1);\n+\t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n+\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n \t} else {\n \t\tstrbuf_realpath(ref_common_dir, payload, 1);\n \t}\n@@ -3600,6 +3598,4 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\tBUG(\"worktree path does not contain slash\");\n \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n \t}\n-\n-\tstrbuf_release(&sb);\n }\n\n-- \n2.55.0.rc0.786.g65d90a0328.dirty\n\n"},{"id":"545850","messageId":"ajP7W7KsXz4Wk262@denethor","threadId":"65786","inReplyTo":"ajOJM8EvGWWkYNuL@pks.im","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-18T14:15:00Z","receivedAt":"2026-06-18T14:15:04Z","isPatch":true,"body":"On 26/06/18 07:59AM, Patrick Steinhardt wrote:\n> On Wed, Jun 17, 2026 at 01:41:40PM -0500, Justin Tobler wrote:\n> > Is this really the best signal to indicate that a repository ref store\n> > has not been initialized? Temporarily setting the storage format to\n> > REF_STORAGE_FORMAT_UNKNOWN feels rather awkward and suggests to me that\n> > `include_by_branch()` probably shouldn't be using it to begin with if\n> > its not reliable.\n> \n> True, but we don't really have a better signal to the best of my\n> knowledge. Ideally, we'd be able to use the existence `r->refs_private`\n> as signal. But that doesn't really work as the reference database is\n> lazily constructed, and the recursion happens in the exact function that\n> would construct it in the first place. And there indeed are cases where\n> reading the configuration is the first caller of `get_main_ref_store()`.\n\nOk, my first thought was also whether we could use the existence of the\nref store as a signal, but I guess that won't work here.\n\n> My first internal iteration tried to make this non-lazily constructed so\n> that we can use it as a proper signal. But that led to a bunch of\n> problems where we now parsed configuration way earlier than we currently\n> do, and that in turn led to all kinds of errors. I was able to fix all\n> of those errors except one: we expect `git config set` to work in a\n> misconfigured repository so that the user can fix the misconfig without\n> having to manually edit the Git configuration files. But when\n> constructing the refdb eagerly we will die early in such cases.\n> \n> We could again work around that issue, but that unfortunately evolved\n> into a proper mess that I eventually discarded as unworkable. I think\n> this is an inherent design flaw: constructing the refdb requires us to\n> be able to parse the configuration, but constructing the configuration\n> may require us to construct the refdb. So this awkwardness is built into\n> Git's design, unfortunately.\n> \n> So I'd really love to have a better signal, as I fully agree that the\n> above workaround is nothing more but a hack. But I'm just not sure what\n> that signal would be. And this version here does exactly what we want:\n> we honor \"onbranch\" conditionals in all cases, except when constructing\n> the main reference store. Even if it's ugly.\n\nCould we embed an `initialized` boolean in `struct ref_store` that gets\nset when the ref store is properly initialized and use that as a signal\ninstead? I'm not sure how complex introducing this would be though.\n\n-Justin\n"},{"id":"545854","messageId":"ajQF1yyCUOdzC4Jq@pks.im","threadId":"65786","inReplyTo":"ajP7W7KsXz4Wk262@denethor","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-18T14:51:03Z","receivedAt":"2026-06-18T14:51:10Z","isPatch":true,"body":"On Thu, Jun 18, 2026 at 09:15:00AM -0500, Justin Tobler wrote:\n> On 26/06/18 07:59AM, Patrick Steinhardt wrote:\n> > On Wed, Jun 17, 2026 at 01:41:40PM -0500, Justin Tobler wrote:\n> > > Is this really the best signal to indicate that a repository ref store\n> > > has not been initialized? Temporarily setting the storage format to\n> > > REF_STORAGE_FORMAT_UNKNOWN feels rather awkward and suggests to me that\n> > > `include_by_branch()` probably shouldn't be using it to begin with if\n> > > its not reliable.\n> > \n> > True, but we don't really have a better signal to the best of my\n> > knowledge. Ideally, we'd be able to use the existence `r->refs_private`\n> > as signal. But that doesn't really work as the reference database is\n> > lazily constructed, and the recursion happens in the exact function that\n> > would construct it in the first place. And there indeed are cases where\n> > reading the configuration is the first caller of `get_main_ref_store()`.\n> \n> Ok, my first thought was also whether we could use the existence of the\n> ref store as a signal, but I guess that won't work here.\n> \n> > My first internal iteration tried to make this non-lazily constructed so\n> > that we can use it as a proper signal. But that led to a bunch of\n> > problems where we now parsed configuration way earlier than we currently\n> > do, and that in turn led to all kinds of errors. I was able to fix all\n> > of those errors except one: we expect `git config set` to work in a\n> > misconfigured repository so that the user can fix the misconfig without\n> > having to manually edit the Git configuration files. But when\n> > constructing the refdb eagerly we will die early in such cases.\n> > \n> > We could again work around that issue, but that unfortunately evolved\n> > into a proper mess that I eventually discarded as unworkable. I think\n> > this is an inherent design flaw: constructing the refdb requires us to\n> > be able to parse the configuration, but constructing the configuration\n> > may require us to construct the refdb. So this awkwardness is built into\n> > Git's design, unfortunately.\n> > \n> > So I'd really love to have a better signal, as I fully agree that the\n> > above workaround is nothing more but a hack. But I'm just not sure what\n> > that signal would be. And this version here does exactly what we want:\n> > we honor \"onbranch\" conditionals in all cases, except when constructing\n> > the main reference store. Even if it's ugly.\n> \n> Could we embed an `initialized` boolean in `struct ref_store` that gets\n> set when the ref store is properly initialized and use that as a signal\n> instead? I'm not sure how complex introducing this would be though.\n\nWe could, but I'm not sure what that would really buy us. It would\nbasically be one more bit of state that we have to track going forward,\nand thus one more source of inconsistencies.\n\nPatrick\n"},{"id":"545862","messageId":"ajQK4vrkc1HVujFh@denethor","threadId":"65786","inReplyTo":"ajQF1yyCUOdzC4Jq@pks.im","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-18T15:53:30Z","receivedAt":"2026-06-18T15:53:34Z","isPatch":true,"body":"On 26/06/18 04:51PM, Patrick Steinhardt wrote:\n> On Thu, Jun 18, 2026 at 09:15:00AM -0500, Justin Tobler wrote:\n> > Could we embed an `initialized` boolean in `struct ref_store` that gets\n> > set when the ref store is properly initialized and use that as a signal\n> > instead? I'm not sure how complex introducing this would be though.\n> \n> We could, but I'm not sure what that would really buy us. It would\n> basically be one more bit of state that we have to track going forward,\n> and thus one more source of inconsistencies.\n\nMy naive thought here is that if the ref store knows when it is\ninitialized, this could be used as a more reliable signal by\n`include_by_branch()`. I guess the problem though would be that, at that\npoint in time, we are still inside `ref_store_init()` and thus the ref\nstore is not fully initialized anyways. \n\nI was hoping we could avoid the hack of temporarily setting the ref\nformat here, but introducing state specific to tracking whether its ok\nto parse onbranch conditions in the config is probably not worth it I\nguess.\n\n-Justin\n"},{"id":"545869","messageId":"20260618164035.GA1218204@coredump.intra.peff.net","threadId":"65786","inReplyTo":"20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-7-f4854aa99859@pks.im","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-18T16:40:35Z","receivedAt":"2026-06-18T16:40:36Z","isPatch":true,"body":"On Mon, Jun 15, 2026 at 03:56:53PM +0200, Patrick Steinhardt wrote:\n\n> When we have an \"onbranch\" condition we need to ask the reference\n> database whether HEAD currently points at the configured branch. This\n> unfortunately creates a chicken-and-egg problem:\n> \n>   - The reference database needs to read the configuration so that it\n>     can configure itself.\n> \n>   - The configuration needs to construct a reference database to fully\n>     parse all of its conditionals.\n> \n> The way we handle this is by simply excluding \"onbranch\" conditionals\n> when we haven't yet configured the reference database.\n\nMy gut feeling upon reading this is that some part of the config reading\nis being done wrong to create this chicken-and-egg situation.\n\nI'd expect the ref database config (like the ref format) to be read not\nthrough the regular config subsystem, but via read_repository_format()\nand friends. And while that does build on the regular config code, it\nshould never enable includes at all. So includeIf.onbranch:foo.path is\njust another uninteresting config key to it.\n\nIn other words, there should be two passes over the config file: one to\nload basic repository information (and not respect includes), and one to\nactually load what we think of as user-visible config[1].\n\nAnd it seems to work. If I do this:\n\ndiff --git a/config.c b/config.c\nindex 45144f73c5..343af2cf9a 100644\n--- a/config.c\n+++ b/config.c\n@@ -303,7 +303,7 @@ static int include_by_branch(struct config_include_data *data,\n \tconst char *refname, *shortname;\n \n \tif (!data->repo || data->repo->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\treturn 0;\n+\t\tBUG(\"chicken and egg\");\n \n \trefname = refs_resolve_ref_unsafe(get_main_ref_store(data->repo),\n \t\t\t\t\t  \"HEAD\", 0, NULL, &flags);\n\nand then:\n\n  git config includeIf.onbranch:main.path alt-config\n  git config -f .git/alt-config foo.bar baz\n  git config foo.bar\n\nthen we correctly read the value without triggering this code path.\n\nLooking back at the last commit that touched include_by_branch(), the\nproblem does not appear to be about a chicken-and-egg at all, though. It\nis about reading config with includes when there is _no_ repository at\nall. I.e., this:\n\n  git config -f main-config includeIf.onbranch:main.path alt-config\n  git config -f  alt-config foo.bar baz\n  GIT_DIR=/does/not/exist git.compile config --include -f main-config foo.bar\n\nwill trigger that BUG() marker, and quietly returning \"no match\" (like\nthe current code does) is the right thing.\n\nLooking below...\n\n> The consequence is that we have recursion:\n> \n>   1. We call `get_main_ref_store()`.\n> \n>   2. We don't yet have a reference store, so we call `ref_store_init()`.\n> \n>   3. We parse the configuration required for the reference store.\n> \n>   4. We eventually end up in `include_by_branch()`.\n> \n>   5. We have already configured the reference storage format, so we end\n>      up calling `get_main_ref_store()` again.\n\nAh, the culprit seems to be ref_store_init() calling into the regular\nconfig parser via repo_settings_get_log_all_ref_updates(). But that\nfeels weird to me. Either:\n\n  1. It is application config that should not be something we need to\n     load in order to initialize the backend. We could lazy-load it\n     later, or rely on higher level code to set the option.\n\n  2. It is crucial to the ref backend functioning, in which case we\n     ought to be reading it alongside core.repositoryFormatVersion, etc.\n\n-Peff\n"},{"id":"545919","messageId":"ajTggBKIzgSpp99X@pks.im","threadId":"65786","inReplyTo":"20260618164035.GA1218204@coredump.intra.peff.net","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T06:25:42Z","receivedAt":"2026-06-19T06:25:54Z","isPatch":true,"body":"On Thu, Jun 18, 2026 at 12:40:35PM -0400, Jeff King wrote:\n> On Mon, Jun 15, 2026 at 03:56:53PM +0200, Patrick Steinhardt wrote:\n[snip]\n> I'd expect the ref database config (like the ref format) to be read not\n> through the regular config subsystem, but via read_repository_format()\n> and friends. And while that does build on the regular config code, it\n> should never enable includes at all. So includeIf.onbranch:foo.path is\n> just another uninteresting config key to it.\n\nThis feels rather painful though, as we'd now have to do this for every\nsingle backend that we know about. Also, I think not enabling includes\nis an overly broad fix: there isn't any reason why \"includeif.gitdir\"\nand all the other conditions shouldn't apply. We really only want to\ndisable \"onbranch\".\n\n[snip]\n> > The consequence is that we have recursion:\n> > \n> >   1. We call `get_main_ref_store()`.\n> > \n> >   2. We don't yet have a reference store, so we call `ref_store_init()`.\n> > \n> >   3. We parse the configuration required for the reference store.\n> > \n> >   4. We eventually end up in `include_by_branch()`.\n> > \n> >   5. We have already configured the reference storage format, so we end\n> >      up calling `get_main_ref_store()` again.\n> \n> Ah, the culprit seems to be ref_store_init() calling into the regular\n> config parser via repo_settings_get_log_all_ref_updates(). But that\n> feels weird to me. Either:\n> \n>   1. It is application config that should not be something we need to\n>      load in order to initialize the backend. We could lazy-load it\n>      later, or rely on higher level code to set the option.\n\nI actually tried lazy-loading, but I found it to be quite painful\noverall, as the above setting isn't the only one we use. The reftable\nbackend for example has a bunch of additional settings that it reads.\n\nWe could of course start lazy-loading all of these. But that may not\nwork for future backends that really _need_ to parse some configuration\nat initiation time.\n\n>   2. It is crucial to the ref backend functioning, in which case we\n>      ought to be reading it alongside core.repositoryFormatVersion, etc.\n\nI think ideally, we'd have a way to read the repository configuration\nthat explicitly disables parsing includes. We could for example extend\n`struct config_options` to have a new \"ignore_refdb\" toggle then\nexplicitly use that in the reference backends.\n\nI'll give that a try.\n\nPatrick\n"},{"id":"545936","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH v4 00/10] refs: stop using `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:48Z","receivedAt":"2026-06-19T11:28:05Z","isPatch":true,"body":"Hi,\n\nthis patch series is a follow-up of the discussion at [1]. It converts\nthe reference backends to always use absolute paths internally, which\nthen allows us to drop the calls to `chdir_notify_reparent()`.\n\nUnfortunately, the series has grown quite a bit larger than anticipated.\nThis is due to a couple of weirdnesses in how the reference database is\nconstructed with an \"onbranch\" condition. We essentially construct the\nrefdb twice and loose one, but we never noticed because the chdir\nnotification subsystem kept the pointer to it reachable.\n\nNote that the first couple patches that touch \"setup.c\" aren't strictly\nrequired. They are a remnant of a previous iteration where I tried to\nsolve the issue in a different way. But I ultimately figured that these\nchanges are worth it by themselves as they simplify \"setup.c\" a bit.\n\nThis series is built on top of 1ff279f340 (The 13th batch, 2026-06-09)\nwith ps/setup-centralize-odb-creation at 42b9d3dc9d (setup: construct\nobject database in `apply_repository_format()`, 2026-06-04) merged into\nit.\n\nChanges in v4:\n  - Fix the \"onbranch\" recursion at the root of the problem by\n    explicitly disabling the use of the ref store when parsing\n    configuration at ref store initialization time.\n  - Link to v3: https://patch.msgid.link/20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im\n\nChanges in v3:\n  - Reduce the scope of applying the GIT_REFERENCE_BACKEND environment\n    variable even further so that we really only do this when we end up\n    applying the reference format.\n  - Fix a commit message that still referred to the dropped last commit.\n  - Link to v2: https://patch.msgid.link/20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im\n\nChanges in v2:\n  - Drop the last patch. This seemingly destroys the whole purpose of\n    the patch series, but after Peff's hint that this is actually a\n    performance optimization I'm less inclined to drop the chdir_notify\n    infra. I still think that the remainder of the patches make sense\n    standalone, as they simplify \"setup.c\" and clean memory leaks. Going\n    forward I'd like to investigate the idea of introducing a `struct\n    fsroot` infrastructure that uses the platform-equivalent of openat\n    et al.\n  - Improve a couple of commit messages.\n  - Link to v1: https://patch.msgid.link/20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <aifAVpxanV31KUpC@pks.im>\n\n---\nPatrick Steinhardt (10):\n      setup: inline `check_and_apply_repository_format()`\n      setup: stop applying repository format twice\n      setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n      refs: unregister reference stores from \"chdir_notify\"\n      chdir-notify: drop unused `chdir_notify_reparent()`\n      repository: free main reference database\n      refs: move parsing of \"core.logAllRefUpdates\" back into ref stores\n      refs/reftable-backend: manually parse \"core.sharedRepository\"\n      refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n      refs: drop local buffer in `refs_compute_filesystem_location()`\n\n builtin/checkout.c      |   7 ++-\n chdir-notify.c          |  26 ------------\n chdir-notify.h          |   6 +--\n config.c                |   4 +-\n config.h                |   1 +\n path.c                  |  11 ++---\n path.h                  |   2 +-\n refs.c                  |  25 ++++++++---\n refs.h                  |   9 ++++\n refs/files-backend.c    |  48 ++++++++++++++++++---\n refs/packed-backend.c   |  16 ++++++-\n refs/refs-internal.h    |   6 ---\n refs/reftable-backend.c |  50 +++++++++++++++++-----\n repo-settings.c         |  16 -------\n repo-settings.h         |   9 ----\n repository.c            |   5 +++\n setup.c                 | 110 +++++++++++++++++++++---------------------------\n 17 files changed, 192 insertions(+), 159 deletions(-)\n\nRange-diff versus v3:\n\n 1:  3ac83ba983 =  1:  3ae112f84b setup: inline `check_and_apply_repository_format()`\n 2:  b6b15770eb =  2:  d03fb25a01 setup: stop applying repository format twice\n 3:  5850f0602d =  3:  f437af7ce6 setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n 4:  e4b12483b4 =  4:  7704b7e5db refs: unregister reference stores from \"chdir_notify\"\n 5:  4a78c5080a =  5:  545fe82dda chdir-notify: drop unused `chdir_notify_reparent()`\n 6:  3f8ae36acc =  6:  5ac9f8c2b3 repository: free main reference database\n 7:  2a22f9a2e0 <  -:  ---------- refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n -:  ---------- >  7:  0482470af1 refs: move parsing of \"core.logAllRefUpdates\" back into ref stores\n -:  ---------- >  8:  1b2f9d4ff9 refs/reftable-backend: manually parse \"core.sharedRepository\"\n -:  ---------- >  9:  c7ec7d887f refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n 8:  6bc943659d = 10:  5fb782268b refs: drop local buffer in `refs_compute_filesystem_location()`\n\n---\nbase-commit: 255322df35357168daefec8523a3cdc849edd6c1\nchange-id: 20260609-b4-pks-refs-avoid-chdir-notify-reparent-a4eaf1edbcab\n\n"},{"id":"545937","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-1-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 01/10] setup: inline `check_and_apply_repository_format()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:49Z","receivedAt":"2026-06-19T11:28:07Z","isPatch":true,"body":"We have two callsites of `check_and_apply_repository_format()`. In a\nsubsequent commit we'll want to adapt one of those callsites to change\nthe order in which we read and apply the repository format, at which\npoint the helper function will not really be a good fit for us anymore.\n\nInline the function to both of the callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 47 ++++++++++++++++-------------------------------\n 1 file changed, 16 insertions(+), 31 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex b4652651df..a9db1f2c23 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo,\n \treturn 0;\n }\n \n-/*\n- * Check the repository format version in the path found in repo_get_git_dir(repo),\n- * and die if it is a version we don't understand. Generally one would\n- * set_git_dir() before calling this, and use it only for \"are we in a valid\n- * repo?\".\n- *\n- * If successful and fmt is not NULL, fill fmt with data.\n- */\n-static void check_and_apply_repository_format(struct repository *repo,\n-\t\t\t\t\t      struct repository_format *fmt,\n-\t\t\t\t\t      enum apply_repository_format_flags flags)\n-{\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\n-\tif (!fmt)\n-\t\tfmt = &repo_fmt;\n-\n-\tcheck_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);\n-\tif (apply_repository_format(repo, fmt, flags, &err) < 0)\n-\t\tdie(\"%s\", err.buf);\n-\tstartup_info->have_repository = 1;\n-\n-\tclear_repository_format(&repo_fmt);\n-}\n-\n const char *enter_repo(struct repository *repo, const char *path, unsigned flags)\n {\n \tstatic struct strbuf validated_path = STRBUF_INIT;\n@@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags\n \t}\n \n \tif (is_git_directory(\".\")) {\n+\t\tstruct repository_format fmt = REPOSITORY_FORMAT_INIT;\n+\t\tstruct strbuf err = STRBUF_INIT;\n+\n \t\tset_git_dir(repo, \".\", 0);\n-\t\tcheck_and_apply_repository_format(repo, NULL,\n-\t\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n+\t\tcheck_repository_format_gently(\".\", &fmt, NULL);\n+\t\tif (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\t\tdie(\"%s\", err.buf);\n+\t\tstartup_info->have_repository = 1;\n+\n+\t\tclear_repository_format(&fmt);\n+\t\tstrbuf_release(&err);\n \t\treturn path;\n \t}\n \n@@ -2820,6 +2802,7 @@ int init_db(struct repository *repo,\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n \n \tif (real_git_dir) {\n \t\tstruct stat st;\n@@ -2846,9 +2829,10 @@ int init_db(struct repository *repo,\n \t * config file, so this will not fail.  What we are catching\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n-\tcheck_and_apply_repository_format(repo, &repo_fmt,\n-\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n-\n+\tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\tdie(\"%s\", err.buf);\n+\tstartup_info->have_repository = 1;\n \trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n@@ -2904,6 +2888,7 @@ int init_db(struct repository *repo,\n \t}\n \n \tclear_repository_format(&repo_fmt);\n+\tstrbuf_release(&err);\n \tfree(original_git_dir);\n \treturn 0;\n }\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545938","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-2-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 02/10] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:50Z","receivedAt":"2026-06-19T11:28:08Z","isPatch":true,"body":"When discovering the repository in \"setup.c\" we apply the final\nrepository format multiple times:\n\n  - Once via `repository_format_configure()`, where we apply the hash\n    algorithm and ref storage format to both `struct repository_format`\n    and `struct repository`.\n\n  - And once via `apply_repository_format()`, where we apply these two\n    settings from `struct repository_format` to `struct repository`.\n\nWith the current flow both of these are in fact necessary. But this is\nonly because we call `repository_format_configure()` after we have\ncalled `apply_repository_format()`. Consequently, if we only changed the\nrepository format in `repository_format_configure()` it would never\npropagate to the repository.\n\nRefactor the code so that we first configure the repository format\nbefore applying it to the repository so that we can stop setting the\nhash and reference storage format multiple times.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 9 ++-------\n 1 file changed, 2 insertions(+), 7 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a9db1f2c23..2748155964 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n \treturn ret;\n }\n \n-static void repository_format_configure(struct repository *repo,\n-\t\t\t\t\tstruct repository_format *repo_fmt,\n+static void repository_format_configure(struct repository_format *repo_fmt,\n \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n {\n \tstruct default_format_config cfg = {\n@@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n \t\trepo_fmt->hash_algo = cfg.hash;\n \t}\n-\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n \n \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n \tif (repo_fmt->version >= 0 &&\n@@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n \n \t\tfree(backend);\n \t}\n-\n-\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n-\t\t\t\t    repo_fmt->ref_storage_payload);\n }\n \n int init_db(struct repository *repo,\n@@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n \t\tdie(\"%s\", err.buf);\n \tstartup_info->have_repository = 1;\n-\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545939","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-3-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 03/10] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:51Z","receivedAt":"2026-06-19T11:28:11Z","isPatch":true,"body":"When discovering a repository we eventually also apply the\n\"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\ntwo problems with that:\n\n  - We do this unconditionally, which is rather pointless: we really\n    only have to configure the repository when we have found one.\n\n  - We have already applied the repository format at that point in time,\n    so we need to manually reapply it.\n\nMove the logic around so that we only apply the environment variable\nwhen a repository was discovered. This also allows us to drop the\nexplcit call to `repo_set_ref_storage_format()` because we now adjust\nthe format before we apply it via `apply_repository_format()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 39 +++++++++++++++++++--------------------\n 1 file changed, 19 insertions(+), 20 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 2748155964..79125db565 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \tstatic struct strbuf cwd = STRBUF_INIT;\n \tstruct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;\n \tconst char *prefix = NULL;\n-\tconst char *ref_backend_uri;\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n \n \t/*\n@@ -2032,6 +2031,25 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \n \t\tif (startup_info->have_repository) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n+\t\t\tconst char *ref_backend_uri;\n+\n+\t\t\t/*\n+\t\t\t * The env variable should override the repository config\n+\t\t\t * for 'extensions.refStorage'.\n+\t\t\t */\n+\t\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n+\t\t\tif (ref_backend_uri) {\n+\t\t\t\tchar *format;\n+\n+\t\t\t\tfree(repo_fmt.ref_storage_payload);\n+\n+\t\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n+\t\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n+\t\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\t\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n+\n+\t\t\t\tfree(format);\n+\t\t\t}\n \n \t\t\tif (apply_repository_format(repo, &repo_fmt,\n \t\t\t\t\t\t    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n@@ -2057,25 +2075,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n-\t/*\n-\t * The env variable should override the repository config\n-\t * for 'extensions.refStorage'.\n-\t */\n-\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n-\tif (ref_backend_uri) {\n-\t\tchar *backend, *payload;\n-\t\tenum ref_storage_format format;\n-\n-\t\tparse_reference_uri(ref_backend_uri, &backend, &payload);\n-\t\tformat = ref_storage_format_by_name(backend);\n-\t\tif (format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\t\tdie(_(\"unknown ref storage format: '%s'\"), backend);\n-\t\trepo_set_ref_storage_format(repo, format, payload);\n-\n-\t\tfree(backend);\n-\t\tfree(payload);\n-\t}\n-\n \tsetup_original_cwd(repo);\n \n \tstrbuf_release(&dir);\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545940","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-4-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 04/10] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:52Z","receivedAt":"2026-06-19T11:28:14Z","isPatch":true,"body":"When creating reference stores we register them with the \"chdir_notify\"\nsubsystem. This is required because some of the paths we track may be\nrelative paths, so we have to reparent them in case the current working\ndirectory changes.\n\nBut while we register the reference stores, we never unregister them.\nThis can have multiple outcomes:\n\n  - For a repository's main reference database we essentially keep the\n    pointer alive. We never free that database, either, and our leak\n    checker doesn't notice because it's still registered.\n\n  - For submodule and worktree reference databases we do eventually free\n    them in `repo_clear()`, so we may keep pointers to free'd memory\n    registered. We never notice though as we don't tend to chdir around\n    in the middle of the process.\n\nWe never noticed either of these symptoms, but they are obviously bad.\n\nPartially fix those issues by unregistering the reference stores when\nreleasing them. The leak of the main reference database will be fixed in\na subsequent commit.\n\nNote that this requires us to use `chdir_notify_register()` instead of\n`chdir_notify_reparent()`, as there is no infrastructure to unregister the\nlatter.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c    | 22 +++++++++++++++++++---\n refs/packed-backend.c   | 16 +++++++++++++++-\n refs/reftable-backend.c | 16 +++++++++++++++-\n 3 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex a4c7858787..296981584b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n+static void files_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t     const char *old_cwd,\n+\t\t\t\t     const char *new_cwd,\n+\t\t\t\t     void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n+\tfree(refs->gitcommondir);\n+\trefs->gitcommondir = tmp;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n-\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n-\t\t\t      &refs->gitcommondir);\n+\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n \n@@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n+\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 0acde48c45..499cb55dfa 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n+static void packed_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *payload)\n+{\n+\tstruct packed_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n+\tfree(refs->path);\n+\trefs->path = tmp;\n+}\n+\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n+\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n \treturn ref_store;\n }\n \n@@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n+\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4ae22922de..8c93070677 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static void reftable_be_reparent(const char *name UNUSED,\n+\t\t\t\t const char *old_cwd,\n+\t\t\t\t const char *new_cwd,\n+\t\t\t\t void *payload)\n+{\n+\tstruct reftable_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+}\n+\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n+\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n \n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n@@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n+\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545941","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-5-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 05/10] chdir-notify: drop unused `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:53Z","receivedAt":"2026-06-19T11:28:16Z","isPatch":true,"body":"With the preceding commit we've removed all callers of\n`chdir_notify_reparent()`, so the function is unused now. Drop it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n chdir-notify.c | 26 --------------------------\n chdir-notify.h |  6 +-----\n 2 files changed, 1 insertion(+), 31 deletions(-)\n\ndiff --git a/chdir-notify.c b/chdir-notify.c\nindex f8bfe3cbef..1237a45e2e 100644\n--- a/chdir-notify.c\n+++ b/chdir-notify.c\n@@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t}\n }\n \n-static void reparent_cb(const char *name,\n-\t\t\tconst char *old_cwd,\n-\t\t\tconst char *new_cwd,\n-\t\t\tvoid *data)\n-{\n-\tchar **path = data;\n-\tchar *tmp = *path;\n-\n-\tif (!tmp)\n-\t\treturn;\n-\n-\t*path = reparent_relative_path(old_cwd, new_cwd, tmp);\n-\tfree(tmp);\n-\n-\tif (name) {\n-\t\ttrace_printf_key(&trace_setup_key,\n-\t\t\t\t \"setup: reparent %s to '%s'\",\n-\t\t\t\t name, *path);\n-\t}\n-}\n-\n-void chdir_notify_reparent(const char *name, char **path)\n-{\n-\tchdir_notify_register(name, reparent_cb, path);\n-}\n-\n int chdir_notify(const char *new_cwd)\n {\n \tstruct strbuf old_cwd = STRBUF_INIT;\ndiff --git a/chdir-notify.h b/chdir-notify.h\nindex 81eb69d846..36b4114472 100644\n--- a/chdir-notify.h\n+++ b/chdir-notify.h\n@@ -19,10 +19,7 @@\n  *   chdir_notify_register(\"description\", foo, data);\n  *\n  * In practice most callers will want to move a relative path to the new root;\n- * they can use the reparent_relative_path() helper for that. If that's all\n- * you're doing, you can also use the convenience function:\n- *\n- *   chdir_notify_reparent(\"description\", &my_path);\n+ * they can use the reparent_relative_path() helper for that.\n  *\n  * Whenever a chdir event occurs, that will update my_path (if it's relative)\n  * to adjust for the new cwd by freeing any existing string and allocating a\n@@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name,\n void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data);\n void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t\t\t     void *data);\n-void chdir_notify_reparent(const char *name, char **path);\n \n /*\n  *\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545942","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-6-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 06/10] repository: free main reference database","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:54Z","receivedAt":"2026-06-19T11:28:19Z","isPatch":true,"body":"While we release worktree and submodule reference databases when\nclearing a repository, we don't ever release the main reference\ndatabase. This memory leak went unnoticed because its pointer is\nkept alive by the \"chdir_notify\" subsystem.\n\nFix the memory leak.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex 187dd471c4..e2b5c6712b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->refs_private) {\n+\t\tref_store_release(repo->refs_private);\n+\t\tFREE_AND_NULL(repo->refs_private);\n+\t}\n+\n \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n \t\tref_store_release(e->value);\n \tstrmap_clear(&repo->submodule_ref_stores, 1);\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545943","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-7-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 07/10] refs: move parsing of \"core.logAllRefUpdates\" back into ref stores","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:55Z","receivedAt":"2026-06-19T11:28:21Z","isPatch":true,"body":"In cc42c88945 (refs: extract out reflog config to generic layer,\n2026-05-04) we have refactored how we parse \"core.logAllRefUpdates\" so\nthat it happens in the generic layer. Unfortunately, this has worsened a\npreexisting issue where we may recurse when creating the reference store\nbecause of a chicken-and-egg problem between parsing the configuration\nand evaluating \"onbranch\" conditions.\n\nPrepare for a fix by essentially reverting that change so that we handle\nthis setting in the respective backends again. The backends are already\nparsing other configuration anyway, so by moving the logic back in there\nwe can ensure that all backend configuration is parsed the same way.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c      |  7 +++++--\n refs.c                  | 10 +++++++++-\n refs.h                  |  9 +++++++++\n refs/files-backend.c    | 20 +++++++++++++++++---\n refs/refs-internal.h    |  6 ------\n refs/reftable-backend.c | 20 +++++++++++---------\n repo-settings.c         | 16 ----------------\n repo-settings.h         |  9 ---------\n setup.c                 |  7 ++++++-\n 9 files changed, 57 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex b78b3a1d16..aee84ca897 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -952,10 +952,13 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \tconst char *old_desc, *reflog_msg;\n \tif (opts->new_branch) {\n \t\tif (opts->new_orphan_branch) {\n-\t\t\tenum log_refs_config log_all_ref_updates =\n-\t\t\t\trepo_settings_get_log_all_ref_updates(the_repository);\n+\t\t\tenum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;\n+\t\t\tconst char *value;\n \t\t\tchar *refname;\n \n+\t\t\tif (!repo_config_get_string_tmp(the_repository, \"core.logallrefupdates\", &value))\n+\t\t\t\tlog_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\n \t\t\trefname = mkpathdup(\"refs/heads/%s\", opts->new_orphan_branch);\n \t\t\tif (opts->new_branch_log &&\n \t\t\t    !should_autocreate_reflog(log_all_ref_updates, refname)) {\ndiff --git a/refs.c b/refs.c\nindex d3caa9a633..5b773b1c15 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1053,6 +1053,15 @@ static char *normalize_reflog_message(const char *msg)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n+enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value)\n+{\n+\tif (value && !strcasecmp(value, \"always\"))\n+\t\treturn LOG_REFS_ALWAYS;\n+\telse if (git_config_bool(\"core.logallrefupdates\", value))\n+\t\treturn LOG_REFS_NORMAL;\n+\treturn LOG_REFS_NONE;\n+}\n+\n int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\n \t\t\t     const char *refname)\n {\n@@ -2327,7 +2336,6 @@ static struct ref_store *ref_store_init(struct repository *repo,\n \tstruct ref_store *refs;\n \tstruct ref_store_init_options opts = {\n \t\t.access_flags = flags,\n-\t\t.log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo),\n \t};\n \n \tbe = find_ref_storage_backend(format);\ndiff --git a/refs.h b/refs.h\nindex 71d5c186d0..a381022c77 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -146,6 +146,15 @@ enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs,\n \n int refs_ref_exists(struct ref_store *refs, const char *refname);\n \n+enum log_refs_config {\n+\tLOG_REFS_UNSET = -1,\n+\tLOG_REFS_NONE = 0,\n+\tLOG_REFS_NORMAL,\n+\tLOG_REFS_ALWAYS\n+};\n+\n+enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value);\n+\n int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\n \t\t\t     const char *refname);\n \ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 296981584b..79fb6735e1 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -117,6 +117,21 @@ static void files_ref_store_reparent(const char *name UNUSED,\n \trefs->gitcommondir = tmp;\n }\n \n+static int files_ref_store_config(const char *var, const char *value,\n+\t\t\t\t  const struct config_context *ctx UNUSED,\n+\t\t\t\t  void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\n+\tif (!strcmp(var, \"core.prefersymlinkrefs\")) {\n+\t\trefs->prefer_symlink_refs = git_config_bool(var, value);\n+\t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n+\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\t}\n+\n+\treturn 0;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -141,10 +156,9 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \trefs->packed_ref_store =\n \t\tpacked_ref_store_init(repo, NULL, refs->gitcommondir, opts);\n \trefs->store_flags = opts->access_flags;\n-\trefs->log_all_ref_updates = opts->log_all_ref_updates;\n-\n-\trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n+\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n+\trepo_config(repo, files_ref_store_config, refs);\n \tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex a08d58900e..c3ac7b556f 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -406,12 +406,6 @@ struct ref_store;\n struct ref_store_init_options {\n \t/* The kind of operations that the ref_store is allowed to perform. */\n \tunsigned int access_flags;\n-\n-\t/*\n-\t * Denotes under what conditions reflogs should be created when updating\n-\t * references.\n-\t */\n-\tenum log_refs_config log_all_ref_updates;\n };\n \n /*\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 8c93070677..5115a3f4ce 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -332,34 +332,36 @@ static void fill_reftable_log_record(struct reftable_log_record *log, const stru\n \n static int reftable_be_config(const char *var, const char *value,\n \t\t\t      const struct config_context *ctx,\n-\t\t\t      void *_opts)\n+\t\t\t      void *payload)\n {\n-\tstruct reftable_write_options *opts = _opts;\n+\tstruct reftable_ref_store *refs = payload;\n \n \tif (!strcmp(var, \"reftable.blocksize\")) {\n \t\tunsigned long block_size = git_config_ulong(var, value, ctx->kvi);\n \t\tif (block_size > 16777215)\n \t\t\tdie(\"reftable block size cannot exceed 16MB\");\n-\t\topts->block_size = block_size;\n+\t\trefs->write_options.block_size = block_size;\n \t} else if (!strcmp(var, \"reftable.restartinterval\")) {\n \t\tunsigned long restart_interval = git_config_ulong(var, value, ctx->kvi);\n \t\tif (restart_interval > UINT16_MAX)\n \t\t\tdie(\"reftable block size cannot exceed %u\", (unsigned)UINT16_MAX);\n-\t\topts->restart_interval = restart_interval;\n+\t\trefs->write_options.restart_interval = restart_interval;\n \t} else if (!strcmp(var, \"reftable.indexobjects\")) {\n-\t\topts->skip_index_objects = !git_config_bool(var, value);\n+\t\trefs->write_options.skip_index_objects = !git_config_bool(var, value);\n \t} else if (!strcmp(var, \"reftable.geometricfactor\")) {\n \t\tunsigned long factor = git_config_ulong(var, value, ctx->kvi);\n \t\tif (factor > UINT8_MAX)\n \t\t\tdie(\"reftable geometric factor cannot exceed %u\", (unsigned)UINT8_MAX);\n-\t\topts->auto_compaction_factor = factor;\n+\t\trefs->write_options.auto_compaction_factor = factor;\n \t} else if (!strcmp(var, \"reftable.locktimeout\")) {\n \t\tint64_t lock_timeout = git_config_int64(var, value, ctx->kvi);\n \t\tif (lock_timeout > LONG_MAX)\n \t\t\tdie(\"reftable lock timeout cannot exceed %\"PRIdMAX, (intmax_t)LONG_MAX);\n \t\tif (lock_timeout < 0 && lock_timeout != -1)\n \t\t\tdie(\"reftable lock timeout does not support negative values other than -1\");\n-\t\topts->lock_timeout_ms = lock_timeout;\n+\t\trefs->write_options.lock_timeout_ms = lock_timeout;\n+\t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n+\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n \t}\n \n \treturn 0;\n@@ -398,7 +400,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \n \tbase_ref_store_init(&refs->base, repo, refdir.buf, &refs_be_reftable);\n \tstrmap_init(&refs->worktree_backends);\n-\trefs->log_all_ref_updates = opts->log_all_ref_updates;\n \trefs->store_flags = opts->access_flags;\n \n \tswitch (repo->hash_algo->format_id) {\n@@ -415,8 +416,9 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \trefs->write_options.disable_auto_compact =\n \t\t!git_env_bool(\"GIT_TEST_REFTABLE_AUTOCOMPACTION\", 1);\n \trefs->write_options.lock_timeout_ms = 100;\n+\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n-\trepo_config(repo, reftable_be_config, &refs->write_options);\n+\trepo_config(repo, reftable_be_config, refs);\n \n \t/*\n \t * It is somewhat unfortunate that we have to mirror the default block\ndiff --git a/repo-settings.c b/repo-settings.c\nindex 208e09ff17..f3be3b8c5a 100644\n--- a/repo-settings.c\n+++ b/repo-settings.c\n@@ -177,22 +177,6 @@ void repo_settings_set_big_file_threshold(struct repository *repo, unsigned long\n \trepo->settings.big_file_threshold = value;\n }\n \n-enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo)\n-{\n-\tconst char *value;\n-\n-\tif (!repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value)) {\n-\t\tif (value && !strcasecmp(value, \"always\"))\n-\t\t\treturn LOG_REFS_ALWAYS;\n-\t\telse if (git_config_bool(\"core.logallrefupdates\", value))\n-\t\t\treturn LOG_REFS_NORMAL;\n-\t\telse\n-\t\t\treturn LOG_REFS_NONE;\n-\t}\n-\n-\treturn LOG_REFS_UNSET;\n-}\n-\n int repo_settings_get_warn_ambiguous_refs(struct repository *repo)\n {\n \tprepare_repo_settings(repo);\ndiff --git a/repo-settings.h b/repo-settings.h\nindex cad9c3f0cc..e5253ead02 100644\n--- a/repo-settings.h\n+++ b/repo-settings.h\n@@ -16,13 +16,6 @@ enum fetch_negotiation_setting {\n \tFETCH_NEGOTIATION_NOOP,\n };\n \n-enum log_refs_config {\n-\tLOG_REFS_UNSET = -1,\n-\tLOG_REFS_NONE = 0,\n-\tLOG_REFS_NORMAL,\n-\tLOG_REFS_ALWAYS\n-};\n-\n struct repo_settings {\n \tint initialized;\n \n@@ -86,8 +79,6 @@ struct repo_settings {\n void prepare_repo_settings(struct repository *r);\n void repo_settings_clear(struct repository *r);\n \n-/* Read the value for \"core.logAllRefUpdates\". */\n-enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo);\n /* Read the value for \"core.warnAmbiguousRefs\". */\n int repo_settings_get_warn_ambiguous_refs(struct repository *repo);\n /* Read the value for \"core.hooksPath\". */\ndiff --git a/setup.c b/setup.c\nindex 79125db565..0c6efb0560 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2584,10 +2584,15 @@ static int create_default_files(struct repository *repo,\n \tif (is_bare_repository())\n \t\trepo_config_set(repo, \"core.bare\", \"true\");\n \telse {\n+\t\tconst char *value;\n+\n \t\trepo_config_set(repo, \"core.bare\", \"false\");\n+\n \t\t/* allow template config file to override the default */\n-\t\tif (repo_settings_get_log_all_ref_updates(repo) == LOG_REFS_UNSET)\n+\t\tif (repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value) ||\n+\t\t    refs_parse_log_all_ref_updates_config(value) == LOG_REFS_UNSET)\n \t\t\trepo_config_set(repo, \"core.logallrefupdates\", \"true\");\n+\n \t\tif (needs_work_tree_config(original_git_dir, work_tree))\n \t\t\trepo_config_set(repo, \"core.worktree\", work_tree);\n \t}\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545944","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-8-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 08/10] refs/reftable-backend: manually parse \"core.sharedRepository\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:56Z","receivedAt":"2026-06-19T11:28:25Z","isPatch":true,"body":"We're using `calc_shared_perm()` when creating a reftable repository.\nThis function internally uses `repo_settings_get_shared_repository()`,\nwhich results in the same chicken-and-egg problem as mentioned in the\npreceding commit.\n\nPrepare for a fix by handling parsing of \"core.sharedRepository\"\nmanually in `reftable_be_config()` so that we have full control over how\nexactly this configuration is read.\n\nNote that this change requires a small reording in \"setup.c\" when\ncreating the repositroy, as we only write \"core.sharedRepository\" into\nthe configuration after we've already created the reference database.\nThis is too late though now that we parse the value directly from the\nconfiguration, so we have to reverse the order.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n path.c                  | 11 ++++++-----\n path.h                  |  2 +-\n refs/reftable-backend.c |  8 +++++++-\n setup.c                 |  8 ++++----\n 4 files changed, 18 insertions(+), 11 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex d7e17bf174..c28b057374 100644\n--- a/path.c\n+++ b/path.c\n@@ -736,11 +736,10 @@ char *interpolate_path(const char *path, int real_home)\n \treturn NULL;\n }\n \n-int calc_shared_perm(struct repository *repo,\n-\t\t     int mode)\n+int calc_shared_perm(int shared_repo, int mode)\n {\n \tint tweak;\n-\tint shared_repo = repo_settings_get_shared_repository(repo);\n+\n \tif (shared_repo < 0)\n \t\ttweak = -shared_repo;\n \telse\n@@ -763,13 +762,15 @@ int adjust_shared_perm(struct repository *repo,\n \t\t       const char *path)\n {\n \tint old_mode, new_mode;\n+\tint shared_repository;\n \n-\tif (!repo_settings_get_shared_repository(repo))\n+\tshared_repository = repo_settings_get_shared_repository(repo);\n+\tif (!shared_repository)\n \t\treturn 0;\n \tif (get_st_mode_bits(path, &old_mode) < 0)\n \t\treturn -1;\n \n-\tnew_mode = calc_shared_perm(repo, old_mode);\n+\tnew_mode = calc_shared_perm(shared_repository, old_mode);\n \tif (S_ISDIR(old_mode)) {\n \t\t/* Copy read bits to execute bits */\n \t\tnew_mode |= (new_mode & 0444) >> 2;\ndiff --git a/path.h b/path.h\nindex 0434ba5e07..1188dc4729 100644\n--- a/path.h\n+++ b/path.h\n@@ -145,7 +145,7 @@ const char *git_path_shallow(struct repository *r);\n \n int ends_with_path_components(const char *path, const char *components);\n \n-int calc_shared_perm(struct repository *repo, int mode);\n+int calc_shared_perm(int shared_repository, int mode);\n int adjust_shared_perm(struct repository *repo, const char *path);\n \n char *interpolate_path(const char *path, int real_home);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 5115a3f4ce..ee92bd9c70 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -362,6 +362,11 @@ static int reftable_be_config(const char *var, const char *value,\n \t\trefs->write_options.lock_timeout_ms = lock_timeout;\n \t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n \t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\t} else if (!strcmp(var, \"core.sharedrepository\")) {\n+\t\tmode_t mask = umask(0);\n+\t\tumask(mask);\n+\t\trefs->write_options.default_permissions = calc_shared_perm(git_config_perm(var, value),\n+\t\t\t\t\t\t\t\t\t   0666 & ~mask);\n \t}\n \n \treturn 0;\n@@ -412,7 +417,8 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \tdefault:\n \t\tBUG(\"unknown hash algorithm %d\", repo->hash_algo->format_id);\n \t}\n-\trefs->write_options.default_permissions = calc_shared_perm(repo, 0666 & ~mask);\n+\n+\trefs->write_options.default_permissions = 0666 & ~mask;\n \trefs->write_options.disable_auto_compact =\n \t\t!git_env_bool(\"GIT_TEST_REFTABLE_AUTOCOMPACTION\", 1);\n \trefs->write_options.lock_timeout_ms = 100;\ndiff --git a/setup.c b/setup.c\nindex 0c6efb0560..03ff359070 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2846,10 +2846,6 @@ int init_db(struct repository *repo,\n \treinit = create_default_files(repo, template_dir, original_git_dir,\n \t\t\t\t      &repo_fmt, init_shared_repository);\n \n-\tif (!(flags & INIT_DB_SKIP_REFDB))\n-\t\tcreate_reference_database(repo, initial_branch, flags & INIT_DB_QUIET);\n-\tcreate_object_directory(repo);\n-\n \tif (repo_settings_get_shared_repository(repo)) {\n \t\tchar buf[10];\n \t\t/* We do not spell \"group\" and such, so that\n@@ -2871,6 +2867,10 @@ int init_db(struct repository *repo,\n \t\trepo_config_set(repo, \"receive.denyNonFastforwards\", \"true\");\n \t}\n \n+\tif (!(flags & INIT_DB_SKIP_REFDB))\n+\t\tcreate_reference_database(repo, initial_branch, flags & INIT_DB_QUIET);\n+\tcreate_object_directory(repo);\n+\n \tif (!(flags & INIT_DB_QUIET)) {\n \t\tint len = strlen(git_dir);\n \n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545945","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-9-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 09/10] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:57Z","receivedAt":"2026-06-19T11:28:27Z","isPatch":true,"body":"When we have an \"onbranch\" condition we need to ask the reference\ndatabase whether HEAD currently points at the configured branch. This\nunfortunately creates a chicken-and-egg problem:\n\n  - The reference database needs to read the configuration so that it\n    can configure itself.\n\n  - The configuration needs to construct a reference database to fully\n    parse all of its conditionals.\n\nThe way we handle this is by simply excluding \"onbranch\" conditionals\nwhen we haven't yet configured the reference database.\n\nThe mechanism for this is broken though: to verify whether or not we\nhave configured the reference database we check whether its format is\nset to `REF_STORAGE_UNKNOWN` in `include_by_branch()`. But typically,\nthe format _is_ already known at that time because we set it up during\nrepository discovery in \"setup.c\".\n\nThe consequence is that we recurse:\n\n  1. We call `get_main_ref_store()`.\n\n  2. We don't yet have a reference store, so we call `ref_store_init()`.\n\n  3. We parse the configuration required for the reference store.\n\n  4. We eventually end up in `include_by_branch()`.\n\n  5. We have already configured the reference storage format, so we end\n     up calling `get_main_ref_store()` again.\n\nWe still haven't finished (1) though, so `get_main_ref_store()` will now\ncall `ref_store_init()` a second time. The end result is that we have\nconstructed the same reference store twice.\n\nOf course, as both reference stores would be assigned to `refs_private`,\nwe leak one of those two instances. This never surfaced as an actual\nleak though because the pointer is kept alive by the \"chdir_notify\"\nsubsystem.\n\nThe mechanism to use the configured reference format is quite fragile in\nthe first place. Introduce a new mechanism that allows us to explicitly\nskip evaluation of \"onbranch\" conditions and use it to fix the issue.\nAdd a sanity check in `get_main_ref_store()` to make sure we aren't\nrecursing, which would have failed before the fix.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n config.c                | 4 +++-\n config.h                | 1 +\n refs.c                  | 7 +++++++\n refs/files-backend.c    | 8 +++++++-\n refs/reftable-backend.c | 8 +++++++-\n 5 files changed, 25 insertions(+), 3 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex a1b92fe083..223c252236 100644\n--- a/config.c\n+++ b/config.c\n@@ -302,7 +302,9 @@ static int include_by_branch(struct config_include_data *data,\n \tstruct strbuf pattern = STRBUF_INIT;\n \tconst char *refname, *shortname;\n \n-\tif (!data->repo || data->repo->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\tif (!data->repo ||\n+\t    data->opts->ignore_refs ||\n+\t    data->repo->ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n \t\treturn 0;\n \n \trefname = refs_resolve_ref_unsafe(get_main_ref_store(data->repo),\ndiff --git a/config.h b/config.h\nindex bf47fb3afc..42aedde878 100644\n--- a/config.h\n+++ b/config.h\n@@ -88,6 +88,7 @@ typedef int (*config_parser_event_fn_t)(enum config_event_t type,\n struct config_options {\n \tunsigned int respect_includes : 1;\n \tunsigned int ignore_repo : 1;\n+\tunsigned int ignore_refs : 1;\n \tunsigned int ignore_worktree : 1;\n \tunsigned int ignore_cmdline : 1;\n \tunsigned int system_gently : 1;\ndiff --git a/refs.c b/refs.c\nindex 5b773b1c15..f242e6ca96 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2359,15 +2359,22 @@ void ref_store_release(struct ref_store *ref_store)\n \n struct ref_store *get_main_ref_store(struct repository *r)\n {\n+\tstatic bool initializing;\n+\n \tif (r->refs_private)\n \t\treturn r->refs_private;\n \n \tif (!r->gitdir)\n \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n+\tif (initializing)\n+\t\tBUG(\"main reference store creation is recursing\");\n \n+\tinitializing = true;\n \tr->refs_private = ref_store_init(r, r->ref_storage_format,\n \t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n \tr->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);\n+\tinitializing = false;\n+\n \treturn r->refs_private;\n }\n \ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 79fb6735e1..ce29875cdd 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -141,6 +141,12 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \t\t\t\t\t      const char *gitdir,\n \t\t\t\t\t      const struct ref_store_init_options *opts)\n {\n+\tstruct config_options config_opts = {\n+\t\t.respect_includes = 1,\n+\t\t.ignore_refs = 1,\n+\t\t.commondir = repo->commondir,\n+\t\t.git_dir = repo->gitdir,\n+\t};\n \tstruct files_ref_store *refs = xcalloc(1, sizeof(*refs));\n \tstruct ref_store *ref_store = (struct ref_store *)refs;\n \tstruct strbuf ref_common_dir = STRBUF_INIT;\n@@ -158,7 +164,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \trefs->store_flags = opts->access_flags;\n \trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n-\trepo_config(repo, files_ref_store_config, refs);\n+\tconfig_with_options(files_ref_store_config, refs, NULL, repo, &config_opts);\n \tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex ee92bd9c70..05d4edc6fd 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -390,6 +390,12 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *gitdir,\n \t\t\t\t\t  const struct ref_store_init_options *opts)\n {\n+\tstruct config_options config_opts = {\n+\t\t.respect_includes = 1,\n+\t\t.ignore_refs = 1,\n+\t\t.commondir = repo->commondir,\n+\t\t.git_dir = repo->gitdir,\n+\t};\n \tstruct reftable_ref_store *refs = xcalloc(1, sizeof(*refs));\n \tstruct strbuf ref_common_dir = STRBUF_INIT;\n \tstruct strbuf refdir = STRBUF_INIT;\n@@ -424,7 +430,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \trefs->write_options.lock_timeout_ms = 100;\n \trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n-\trepo_config(repo, reftable_be_config, refs);\n+\tconfig_with_options(reftable_be_config, refs, NULL, repo, &config_opts);\n \n \t/*\n \t * It is somewhat unfortunate that we have to mirror the default block\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"545946","messageId":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-10-a6472be7acc4@pks.im","threadId":"65786","inReplyTo":"20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im","subject":"[PATCH v4 10/10] refs: drop local buffer in `refs_compute_filesystem_location()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-19T11:27:58Z","receivedAt":"2026-06-19T11:28:30Z","isPatch":true,"body":"We're using a local buffer in `refs_compute_filesystem_location()` that\nis only used so that we can fill it and then call `strbuf_realpath()` on\nits result. This roundtrip isn't necessary though: `strbuf_realpath()`\nalready knows to use a single buffer as both input and output at the\nsame time. So all this does is to add a bit of confusion and an extra\nmemory allocation.\n\nDrop the local buffer.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 8 ++------\n 1 file changed, 2 insertions(+), 6 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex f242e6ca96..582dbeff0a 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -3570,8 +3570,6 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\t\t      bool *is_worktree, struct strbuf *refdir,\n \t\t\t\t      struct strbuf *ref_common_dir)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n-\n \t*is_worktree = get_common_dir_noenv(ref_common_dir, gitdir);\n \n \tif (!payload) {\n@@ -3585,8 +3583,8 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t}\n \n \tif (!is_absolute_path(payload)) {\n-\t\tstrbuf_addf(&sb, \"%s/%s\", ref_common_dir->buf, payload);\n-\t\tstrbuf_realpath(ref_common_dir, sb.buf, 1);\n+\t\tstrbuf_addf(ref_common_dir, \"/%s\", payload);\n+\t\tstrbuf_realpath(ref_common_dir, ref_common_dir->buf, 1);\n \t} else {\n \t\tstrbuf_realpath(ref_common_dir, payload, 1);\n \t}\n@@ -3599,6 +3597,4 @@ void refs_compute_filesystem_location(const char *gitdir, const char *payload,\n \t\t\tBUG(\"worktree path does not contain slash\");\n \t\tstrbuf_addf(refdir, \"/worktrees/%s\", wt_id + 1);\n \t}\n-\n-\tstrbuf_release(&sb);\n }\n\n-- \n2.55.0.rc1.722.g2b3ac350e6.dirty\n\n"},{"id":"546090","messageId":"20260621211211.GA2297179@coredump.intra.peff.net","threadId":"65786","inReplyTo":"ajTggBKIzgSpp99X@pks.im","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-21T21:12:11Z","receivedAt":"2026-06-21T21:12:12Z","isPatch":true,"body":"On Fri, Jun 19, 2026 at 08:25:42AM +0200, Patrick Steinhardt wrote:\n\n> On Thu, Jun 18, 2026 at 12:40:35PM -0400, Jeff King wrote:\n> > On Mon, Jun 15, 2026 at 03:56:53PM +0200, Patrick Steinhardt wrote:\n> [snip]\n> > I'd expect the ref database config (like the ref format) to be read not\n> > through the regular config subsystem, but via read_repository_format()\n> > and friends. And while that does build on the regular config code, it\n> > should never enable includes at all. So includeIf.onbranch:foo.path is\n> > just another uninteresting config key to it.\n> \n> This feels rather painful though, as we'd now have to do this for every\n> single backend that we know about. Also, I think not enabling includes\n> is an overly broad fix: there isn't any reason why \"includeif.gitdir\"\n> and all the other conditions shouldn't apply. We really only want to\n> disable \"onbranch\".\n\nSorry, I should probably gone back and edited my email after finishing\nit. I was thinking that you meant not general config, but the specific\nextensions.refStorage key. Which is not really config, but repo metadata\nwe happen to store in the .git/config file. And obviously you cannot\nread any refs until you know what's in that key.\n\nAnd that _is_ read separately while loading the repo config, which I\nthink is right. Other options, like core.logallrefupdates, are handled\nseparately. And I realized halfway through my reply that was probably\nwhat you meant.\n\nI agree those are user-facing config options that should generally\nrespect includes in the normal way. I thinks are a bit funny there,\nthough. See below.\n\n> I actually tried lazy-loading, but I found it to be quite painful\n> overall, as the above setting isn't the only one we use. The reftable\n> backend for example has a bunch of additional settings that it reads.\n> \n> We could of course start lazy-loading all of these. But that may not\n> work for future backends that really _need_ to parse some configuration\n> at initiation time.\n\nYes, obviously there's some true chicken-and-egg issues if there are\nconfig keys that are needed to initialize the backend. But I think there\nare many that are not needed immediately (e.g., because they relate only\nto writes, not reads) but still block loading.\n\nFor example, try this:\n\n  git init\n  git config core.logallrefupdates false\n  git config includeIf.onbranch:main.path alt-config\n  git config -f .git/alt-config core.logallrefupdates true\n  git commit --allow-empty -qm foo\n\n  echo \"git-config => $(git config core.logallrefupdates)\"\n  echo \"reflog => $(git reflog show)\"\n\ngit-config will report the value as true, but git-commit will not\nrespect it. But this used to work! Back when onbranch was added, we'd\ncreate the reflog. Bisecting turns up eafb126456 (environment: stop\nstoring \"core.logAllRefUpdates\" globally, 2024-09-12), which makes\nsense. That commit pushed the config read down into the ref\ninitialization function, which created the chicken-and-egg.\n\nNow the config shown above is a bit silly, and I don't expect anybody to\ndo it in real life. But what worries me is two-fold:\n\n  1. There are some magic variables that just won't work with onbranch\n     includes, but the user doesn't necessarily know what they are.\n\n  2. We try to cache the results of config reads. Is it possible for an\n     \"early\" request like this to cache a state that skipped the\n     onbranch include, and then we use that state to look up other\n     unrelated variables? Or could we see a partially completed state in\n     the cache when we lookup a ref variable?\n\n     I'm not sure. The actual backend lookups use the uncached\n     repo_config() interface (and in your series here, explicitly\n     disables the use of refs during that read). But the\n     core.logallrefupdates lookup uses the cached version, and I think\n     there are others (some of which happen deep under the hood\n     through library calls, like calc_shared_perm()).\n\nI tried to construct a few cases that might tickle this behavior, but\ncouldn't come up with one. But I have a nagging feeling that we are\nmostly getting lucky on some of the ordering, and a seemingly unrelated\nchange could have bad effects.\n\nSorry, I know that's kind of vague and hand-wavy.\n\nI'm not sure I have a specific recommendation for a direction. It just\nfeels like we're piling up hacks to avoid infinite recursion without a\nclear model of what config is read when. I guess if I could suggest\nanything, it would be that ref backends initialize themselves to do\nreads while loading as little config as possible, and then perhaps load\nadditional config through the non-caching repo_config() path.\n\n-Peff\n"},{"id":"546106","messageId":"ajjFAjyGjk6q792L@pks.im","threadId":"65786","inReplyTo":"20260621211211.GA2297179@coredump.intra.peff.net","subject":"Re: [PATCH v2 7/8] refs: fix recursing `get_main_ref_store()` with \"onbranch\" config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T05:15:46Z","receivedAt":"2026-06-22T05:15:53Z","isPatch":true,"body":"On Sun, Jun 21, 2026 at 05:12:11PM -0400, Jeff King wrote:\n> On Fri, Jun 19, 2026 at 08:25:42AM +0200, Patrick Steinhardt wrote:\n> > On Thu, Jun 18, 2026 at 12:40:35PM -0400, Jeff King wrote:\n> > > On Mon, Jun 15, 2026 at 03:56:53PM +0200, Patrick Steinhardt wrote:\n> > I actually tried lazy-loading, but I found it to be quite painful\n> > overall, as the above setting isn't the only one we use. The reftable\n> > backend for example has a bunch of additional settings that it reads.\n> > \n> > We could of course start lazy-loading all of these. But that may not\n> > work for future backends that really _need_ to parse some configuration\n> > at initiation time.\n> \n> Yes, obviously there's some true chicken-and-egg issues if there are\n> config keys that are needed to initialize the backend. But I think there\n> are many that are not needed immediately (e.g., because they relate only\n> to writes, not reads) but still block loading.\n> \n> For example, try this:\n> \n>   git init\n>   git config core.logallrefupdates false\n>   git config includeIf.onbranch:main.path alt-config\n>   git config -f .git/alt-config core.logallrefupdates true\n>   git commit --allow-empty -qm foo\n> \n>   echo \"git-config => $(git config core.logallrefupdates)\"\n>   echo \"reflog => $(git reflog show)\"\n> \n> git-config will report the value as true, but git-commit will not\n> respect it. But this used to work! Back when onbranch was added, we'd\n> create the reflog. Bisecting turns up eafb126456 (environment: stop\n> storing \"core.logAllRefUpdates\" globally, 2024-09-12), which makes\n> sense. That commit pushed the config read down into the ref\n> initialization function, which created the chicken-and-egg.\n> \n> Now the config shown above is a bit silly, and I don't expect anybody to\n> do it in real life. But what worries me is two-fold:\n> \n>   1. There are some magic variables that just won't work with onbranch\n>      includes, but the user doesn't necessarily know what they are.\n> \n>   2. We try to cache the results of config reads. Is it possible for an\n>      \"early\" request like this to cache a state that skipped the\n>      onbranch include, and then we use that state to look up other\n>      unrelated variables? Or could we see a partially completed state in\n>      the cache when we lookup a ref variable?\n> \n>      I'm not sure. The actual backend lookups use the uncached\n>      repo_config() interface (and in your series here, explicitly\n>      disables the use of refs during that read). But the\n>      core.logallrefupdates lookup uses the cached version, and I think\n>      there are others (some of which happen deep under the hood\n>      through library calls, like calc_shared_perm()).\n> \n> I tried to construct a few cases that might tickle this behavior, but\n> couldn't come up with one. But I have a nagging feeling that we are\n> mostly getting lucky on some of the ordering, and a seemingly unrelated\n> change could have bad effects.\n> \n> Sorry, I know that's kind of vague and hand-wavy.\n> \n> I'm not sure I have a specific recommendation for a direction. It just\n> feels like we're piling up hacks to avoid infinite recursion without a\n> clear model of what config is read when. I guess if I could suggest\n> anything, it would be that ref backends initialize themselves to do\n> reads while loading as little config as possible, and then perhaps load\n> additional config through the non-caching repo_config() path.\n\nYeah, I thought more about this issue over the weekend and kind of got\nto the same conclusion. Sure, the current version where we explicitly\nhandle the exclusion of \"onbranch\" conditions is at least less awkward.\nBut I have to agree that it's still not the right fix, as it doesn't\nreally solve the root issue.\n\nTaking a step back: all the values that we currently parse are only\nrelevant when writing new refs. So in theory it should be possible to\nlazy-load all of them on the first write. This should be rather easy to\ndo for the \"files\" backend. But for the \"reftable\" backend this will\nresult in a large refactoring because we require the configuration when\nconstructing the reftable stack.\n\nThat's kind of misdesigned though: the reftable stack shouldn't really\ncare about write options when being constructed. What it needs to know\nabout is the expected hash ID, and any optional stuff like the onreload\ncallback. The write options should then be passed by the caller when we\nactually perform a write.\n\nI'll iterate a bit on this idea and will see where I get. I really\nshouldn't have opened this can of worms.\n\nThanks!\n\nPatrick\n"},{"id":"546123","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im","subject":"[PATCH v5 00/11] refs: fix \"onbranch\" conditions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:22Z","receivedAt":"2026-06-22T08:28:30Z","isPatch":true,"body":"Hi,\n\noriginally, this patch series was a follow-up of the discussion at [1],\nwhere it converted the reference backends to always use absolute paths\ninternally so that we could drop the `chdir_notify_reparent()`\nmachinery. But this focus shifted as we discovered that this led to\nquite a sizeable performance regression.\n\nInstead, the series now focusses on fixing handling of the \"onbranch\"\nconditions. As part of the above work I discovered that we recurse when\ncreating the main reference database in case we have \"onbranch\"\nconditions, and that recursion caused us to construct an ad-hoc\nreference store that we essentially discarded. The leak wasn't ever\ncatched though because the store is kept alive by the `chdir_notify`\ninfrastructure.\n\nThis is a deeper-running issue though: the reference backends respect\nsome configuration guarded by \"onbranch\" conditions, but not all of\nthem. This issue is fixed by this series by lazy-loading all\nconfiguration so that we don't need to read any configuration when we\ninitialize the reference store. This fixes the recursion and makes us\nconsistently honor those \"onbranch\" conditions.\n\nThis series is built on top of 1ff279f340 (The 13th batch, 2026-06-09)\nwith ps/setup-centralize-odb-creation at 42b9d3dc9d (setup: construct\nobject database in `apply_repository_format()`, 2026-06-04) merged into\nit.\n\nChanges in v5:\n  - Fix the \"onbranch\" recursion properly: instead of papering over the\n    issue, this series now refactors reference store initialization to\n    not read any configuration at all anymore. Instead, the config is\n    now parsed lazily. This fixes the recursion, but also makes us\n    respect configuration guarded by \"onbranch\" conditions properly.\n  - Link to v4: https://patch.msgid.link/20260619-b4-pks-refs-avoid-chdir-notify-reparent-v4-0-a6472be7acc4@pks.im\n\nChanges in v4:\n  - Fix the \"onbranch\" recursion at the root of the problem by\n    explicitly disabling the use of the ref store when parsing\n    configuration at ref store initialization time.\n  - Link to v3: https://patch.msgid.link/20260618-b4-pks-refs-avoid-chdir-notify-reparent-v3-0-2a5669e8f486@pks.im\n\nChanges in v3:\n  - Reduce the scope of applying the GIT_REFERENCE_BACKEND environment\n    variable even further so that we really only do this when we end up\n    applying the reference format.\n  - Fix a commit message that still referred to the dropped last commit.\n  - Link to v2: https://patch.msgid.link/20260615-b4-pks-refs-avoid-chdir-notify-reparent-v2-0-f4854aa99859@pks.im\n\nChanges in v2:\n  - Drop the last patch. This seemingly destroys the whole purpose of\n    the patch series, but after Peff's hint that this is actually a\n    performance optimization I'm less inclined to drop the chdir_notify\n    infra. I still think that the remainder of the patches make sense\n    standalone, as they simplify \"setup.c\" and clean memory leaks. Going\n    forward I'd like to investigate the idea of introducing a `struct\n    fsroot` infrastructure that uses the platform-equivalent of openat\n    et al.\n  - Improve a couple of commit messages.\n  - Link to v1: https://patch.msgid.link/20260610-b4-pks-refs-avoid-chdir-notify-reparent-v1-0-56c864b01c43@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <aifAVpxanV31KUpC@pks.im>\n\n---\nPatrick Steinhardt (11):\n      setup: inline `check_and_apply_repository_format()`\n      setup: stop applying repository format twice\n      setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n      refs: unregister reference stores from \"chdir_notify\"\n      chdir-notify: drop unused `chdir_notify_reparent()`\n      repository: free main reference database\n      refs: move parsing of \"core.logAllRefUpdates\" back into ref stores\n      refs/files: lazy-load configuration to fix chicken-and-egg\n      reftable: split up write options\n      refs/reftable: lazy-load configuration to fix chicken-and-egg\n      refs: protect against chicken-and-egg recursion\n\n builtin/checkout.c                  |   7 +-\n chdir-notify.c                      |  26 -----\n chdir-notify.h                      |   6 +-\n refs.c                              |  17 +++-\n refs.h                              |   9 ++\n refs/files-backend.c                |  69 ++++++++++---\n refs/packed-backend.c               |  16 ++-\n refs/refs-internal.h                |   6 --\n refs/reftable-backend.c             | 177 ++++++++++++++++++++-------------\n reftable/reftable-stack.h           |  30 +++++-\n reftable/reftable-writer.h          |  17 +---\n reftable/stack.c                    | 100 ++++++++++++-------\n reftable/stack.h                    |   2 +-\n reftable/writer.c                   |  21 ++--\n reftable/writer.h                   |   1 +\n repo-settings.c                     |  16 ---\n repo-settings.h                     |   9 --\n repository.c                        |   5 +\n setup.c                             | 102 ++++++++-----------\n t/helper/test-reftable.c            |   2 +-\n t/t0600-reffiles-backend.sh         |  21 ++++\n t/t0613-reftable-write-options.sh   |  19 ++++\n t/t1400-update-ref.sh               |  12 +++\n t/unit-tests/lib-reftable.c         |   8 +-\n t/unit-tests/lib-reftable.h         |   2 +\n t/unit-tests/u-reftable-merged.c    |   9 +-\n t/unit-tests/u-reftable-readwrite.c |  38 ++++++--\n t/unit-tests/u-reftable-stack.c     | 189 ++++++++++++++++--------------------\n t/unit-tests/u-reftable-table.c     |   8 +-\n 29 files changed, 555 insertions(+), 389 deletions(-)\n\nRange-diff versus v4:\n\n 1:  a70b0f44b2 =  1:  1a3e7849fb setup: inline `check_and_apply_repository_format()`\n 2:  b33b51748b =  2:  9fee5b6ac2 setup: stop applying repository format twice\n 3:  a22755337a =  3:  8eeaaa2359 setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository\n 4:  848645c3e8 =  4:  19f0e381aa refs: unregister reference stores from \"chdir_notify\"\n 5:  489e274577 =  5:  5b1ec8f62a chdir-notify: drop unused `chdir_notify_reparent()`\n 6:  a9811da5c8 =  6:  44abab07fa repository: free main reference database\n 7:  8de1023c6b =  7:  21d46ff924 refs: move parsing of \"core.logAllRefUpdates\" back into ref stores\n 8:  cb3cf159d2 <  -:  ---------- refs/reftable-backend: manually parse \"core.sharedRepository\"\n 9:  1a7c195c03 <  -:  ---------- refs: fix recursing `get_main_ref_store()` with \"onbranch\" config\n10:  c9b019a1a5 <  -:  ---------- refs: drop local buffer in `refs_compute_filesystem_location()`\n -:  ---------- >  8:  22d65ada3d refs/files: lazy-load configuration to fix chicken-and-egg\n -:  ---------- >  9:  715b090f40 reftable: split up write options\n -:  ---------- > 10:  a941049373 refs/reftable: lazy-load configuration to fix chicken-and-egg\n -:  ---------- > 11:  7ca965fe73 refs: protect against chicken-and-egg recursion\n\n---\nbase-commit: 255322df35357168daefec8523a3cdc849edd6c1\nchange-id: 20260609-b4-pks-refs-avoid-chdir-notify-reparent-a4eaf1edbcab\n\n"},{"id":"546124","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-1-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 01/11] setup: inline `check_and_apply_repository_format()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:23Z","receivedAt":"2026-06-22T08:28:32Z","isPatch":true,"body":"We have two callsites of `check_and_apply_repository_format()`. In a\nsubsequent commit we'll want to adapt one of those callsites to change\nthe order in which we read and apply the repository format, at which\npoint the helper function will not really be a good fit for us anymore.\n\nInline the function to both of the callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 47 ++++++++++++++++-------------------------------\n 1 file changed, 16 insertions(+), 31 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex b4652651df..a9db1f2c23 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1788,32 +1788,6 @@ int apply_repository_format(struct repository *repo,\n \treturn 0;\n }\n \n-/*\n- * Check the repository format version in the path found in repo_get_git_dir(repo),\n- * and die if it is a version we don't understand. Generally one would\n- * set_git_dir() before calling this, and use it only for \"are we in a valid\n- * repo?\".\n- *\n- * If successful and fmt is not NULL, fill fmt with data.\n- */\n-static void check_and_apply_repository_format(struct repository *repo,\n-\t\t\t\t\t      struct repository_format *fmt,\n-\t\t\t\t\t      enum apply_repository_format_flags flags)\n-{\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\n-\tif (!fmt)\n-\t\tfmt = &repo_fmt;\n-\n-\tcheck_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);\n-\tif (apply_repository_format(repo, fmt, flags, &err) < 0)\n-\t\tdie(\"%s\", err.buf);\n-\tstartup_info->have_repository = 1;\n-\n-\tclear_repository_format(&repo_fmt);\n-}\n-\n const char *enter_repo(struct repository *repo, const char *path, unsigned flags)\n {\n \tstatic struct strbuf validated_path = STRBUF_INIT;\n@@ -1887,9 +1861,17 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags\n \t}\n \n \tif (is_git_directory(\".\")) {\n+\t\tstruct repository_format fmt = REPOSITORY_FORMAT_INIT;\n+\t\tstruct strbuf err = STRBUF_INIT;\n+\n \t\tset_git_dir(repo, \".\", 0);\n-\t\tcheck_and_apply_repository_format(repo, NULL,\n-\t\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n+\t\tcheck_repository_format_gently(\".\", &fmt, NULL);\n+\t\tif (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\t\tdie(\"%s\", err.buf);\n+\t\tstartup_info->have_repository = 1;\n+\n+\t\tclear_repository_format(&fmt);\n+\t\tstrbuf_release(&err);\n \t\treturn path;\n \t}\n \n@@ -2820,6 +2802,7 @@ int init_db(struct repository *repo,\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n \n \tif (real_git_dir) {\n \t\tstruct stat st;\n@@ -2846,9 +2829,10 @@ int init_db(struct repository *repo,\n \t * config file, so this will not fail.  What we are catching\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n-\tcheck_and_apply_repository_format(repo, &repo_fmt,\n-\t\t\t\t\t  APPLY_REPOSITORY_FORMAT_HONOR_ENV);\n-\n+\tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n+\t\tdie(\"%s\", err.buf);\n+\tstartup_info->have_repository = 1;\n \trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n@@ -2904,6 +2888,7 @@ int init_db(struct repository *repo,\n \t}\n \n \tclear_repository_format(&repo_fmt);\n+\tstrbuf_release(&err);\n \tfree(original_git_dir);\n \treturn 0;\n }\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546125","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-2-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 02/11] setup: stop applying repository format twice","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:24Z","receivedAt":"2026-06-22T08:28:36Z","isPatch":true,"body":"When discovering the repository in \"setup.c\" we apply the final\nrepository format multiple times:\n\n  - Once via `repository_format_configure()`, where we apply the hash\n    algorithm and ref storage format to both `struct repository_format`\n    and `struct repository`.\n\n  - And once via `apply_repository_format()`, where we apply these two\n    settings from `struct repository_format` to `struct repository`.\n\nWith the current flow both of these are in fact necessary. But this is\nonly because we call `repository_format_configure()` after we have\ncalled `apply_repository_format()`. Consequently, if we only changed the\nrepository format in `repository_format_configure()` it would never\npropagate to the repository.\n\nRefactor the code so that we first configure the repository format\nbefore applying it to the repository so that we can stop setting the\nhash and reference storage format multiple times.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 9 ++-------\n 1 file changed, 2 insertions(+), 7 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a9db1f2c23..2748155964 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2710,8 +2710,7 @@ static int read_default_format_config(const char *key, const char *value,\n \treturn ret;\n }\n \n-static void repository_format_configure(struct repository *repo,\n-\t\t\t\t\tstruct repository_format *repo_fmt,\n+static void repository_format_configure(struct repository_format *repo_fmt,\n \t\t\t\t\tint hash, enum ref_storage_format ref_format)\n {\n \tstruct default_format_config cfg = {\n@@ -2748,7 +2747,6 @@ static void repository_format_configure(struct repository *repo,\n \t} else if (cfg.hash != GIT_HASH_UNKNOWN) {\n \t\trepo_fmt->hash_algo = cfg.hash;\n \t}\n-\trepo_set_hash_algo(repo, repo_fmt->hash_algo);\n \n \tenv = getenv(\"GIT_DEFAULT_REF_FORMAT\");\n \tif (repo_fmt->version >= 0 &&\n@@ -2786,9 +2784,6 @@ static void repository_format_configure(struct repository *repo,\n \n \t\tfree(backend);\n \t}\n-\n-\trepo_set_ref_storage_format(repo, repo_fmt->ref_storage_format,\n-\t\t\t\t    repo_fmt->ref_storage_payload);\n }\n \n int init_db(struct repository *repo,\n@@ -2830,10 +2825,10 @@ int init_db(struct repository *repo,\n \t * is an attempt to reinitialize new repository with an old tool.\n \t */\n \tcheck_repository_format_gently(repo_get_git_dir(repo), &repo_fmt, NULL);\n+\trepository_format_configure(&repo_fmt, hash, ref_storage_format);\n \tif (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n \t\tdie(\"%s\", err.buf);\n \tstartup_info->have_repository = 1;\n-\trepository_format_configure(repo, &repo_fmt, hash, ref_storage_format);\n \n \t/*\n \t * Ensure `core.hidedotfiles` is processed. This must happen after we\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546126","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-3-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 03/11] setup: don't apply \"GIT_REFERENCE_BACKEND\" without a repository","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:25Z","receivedAt":"2026-06-22T08:28:38Z","isPatch":true,"body":"When discovering a repository we eventually also apply the\n\"GIT_REFERENCE_BACKEND\" environment variable to the repository. There's\ntwo problems with that:\n\n  - We do this unconditionally, which is rather pointless: we really\n    only have to configure the repository when we have found one.\n\n  - We have already applied the repository format at that point in time,\n    so we need to manually reapply it.\n\nMove the logic around so that we only apply the environment variable\nwhen a repository was discovered. This also allows us to drop the\nexplcit call to `repo_set_ref_storage_format()` because we now adjust\nthe format before we apply it via `apply_repository_format()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n setup.c | 39 +++++++++++++++++++--------------------\n 1 file changed, 19 insertions(+), 20 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 2748155964..79125db565 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1906,7 +1906,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \tstatic struct strbuf cwd = STRBUF_INIT;\n \tstruct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;\n \tconst char *prefix = NULL;\n-\tconst char *ref_backend_uri;\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n \n \t/*\n@@ -2032,6 +2031,25 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \n \t\tif (startup_info->have_repository) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n+\t\t\tconst char *ref_backend_uri;\n+\n+\t\t\t/*\n+\t\t\t * The env variable should override the repository config\n+\t\t\t * for 'extensions.refStorage'.\n+\t\t\t */\n+\t\t\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n+\t\t\tif (ref_backend_uri) {\n+\t\t\t\tchar *format;\n+\n+\t\t\t\tfree(repo_fmt.ref_storage_payload);\n+\n+\t\t\t\tparse_reference_uri(ref_backend_uri, &format, &repo_fmt.ref_storage_payload);\n+\t\t\t\trepo_fmt.ref_storage_format = ref_storage_format_by_name(format);\n+\t\t\t\tif (repo_fmt.ref_storage_format == REF_STORAGE_FORMAT_UNKNOWN)\n+\t\t\t\t\tdie(_(\"unknown ref storage format: '%s'\"), format);\n+\n+\t\t\t\tfree(format);\n+\t\t\t}\n \n \t\t\tif (apply_repository_format(repo, &repo_fmt,\n \t\t\t\t\t\t    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)\n@@ -2057,25 +2075,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)\n \t\tsetenv(GIT_PREFIX_ENVIRONMENT, \"\", 1);\n \t}\n \n-\t/*\n-\t * The env variable should override the repository config\n-\t * for 'extensions.refStorage'.\n-\t */\n-\tref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);\n-\tif (ref_backend_uri) {\n-\t\tchar *backend, *payload;\n-\t\tenum ref_storage_format format;\n-\n-\t\tparse_reference_uri(ref_backend_uri, &backend, &payload);\n-\t\tformat = ref_storage_format_by_name(backend);\n-\t\tif (format == REF_STORAGE_FORMAT_UNKNOWN)\n-\t\t\tdie(_(\"unknown ref storage format: '%s'\"), backend);\n-\t\trepo_set_ref_storage_format(repo, format, payload);\n-\n-\t\tfree(backend);\n-\t\tfree(payload);\n-\t}\n-\n \tsetup_original_cwd(repo);\n \n \tstrbuf_release(&dir);\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546127","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-4-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 04/11] refs: unregister reference stores from \"chdir_notify\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:26Z","receivedAt":"2026-06-22T08:28:40Z","isPatch":true,"body":"When creating reference stores we register them with the \"chdir_notify\"\nsubsystem. This is required because some of the paths we track may be\nrelative paths, so we have to reparent them in case the current working\ndirectory changes.\n\nBut while we register the reference stores, we never unregister them.\nThis can have multiple outcomes:\n\n  - For a repository's main reference database we essentially keep the\n    pointer alive. We never free that database, either, and our leak\n    checker doesn't notice because it's still registered.\n\n  - For submodule and worktree reference databases we do eventually free\n    them in `repo_clear()`, so we may keep pointers to free'd memory\n    registered. We never notice though as we don't tend to chdir around\n    in the middle of the process.\n\nWe never noticed either of these symptoms, but they are obviously bad.\n\nPartially fix those issues by unregistering the reference stores when\nreleasing them. The leak of the main reference database will be fixed in\na subsequent commit.\n\nNote that this requires us to use `chdir_notify_register()` instead of\n`chdir_notify_reparent()`, as there is no infrastructure to unregister the\nlatter.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c    | 22 +++++++++++++++++++---\n refs/packed-backend.c   | 16 +++++++++++++++-\n refs/reftable-backend.c | 16 +++++++++++++++-\n 3 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex a4c7858787..296981584b 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -100,6 +100,23 @@ static void clear_loose_ref_cache(struct files_ref_store *refs)\n \t}\n }\n \n+static void files_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t     const char *old_cwd,\n+\t\t\t\t     const char *new_cwd,\n+\t\t\t\t     void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->gitcommondir);\n+\tfree(refs->gitcommondir);\n+\trefs->gitcommondir = tmp;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -128,9 +145,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \n \trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n \n-\tchdir_notify_reparent(\"files-backend $GIT_DIR\", &refs->base.gitdir);\n-\tchdir_notify_reparent(\"files-backend $GIT_COMMONDIR\",\n-\t\t\t      &refs->gitcommondir);\n+\tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n \n@@ -182,6 +197,7 @@ static void files_ref_store_release(struct ref_store *ref_store)\n \tfree(refs->gitcommondir);\n \tref_store_release(refs->packed_ref_store);\n \tfree(refs->packed_ref_store);\n+\tchdir_notify_unregister(NULL, files_ref_store_reparent, refs);\n }\n \n static void files_reflog_path(struct files_ref_store *refs,\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 0acde48c45..499cb55dfa 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -211,6 +211,19 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot)\n \treturn snapshot->refs->base.repo->hash_algo->hexsz;\n }\n \n+static void packed_ref_store_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *payload)\n+{\n+\tstruct packed_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->path);\n+\tfree(refs->path);\n+\trefs->path = tmp;\n+}\n+\n /*\n  * Since packed-refs is only stored in the common dir, don't parse the\n  * payload and rely on the files-backend to set 'gitdir' correctly.\n@@ -229,7 +242,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo,\n \n \tstrbuf_addf(&sb, \"%s/packed-refs\", gitdir);\n \trefs->path = strbuf_detach(&sb, NULL);\n-\tchdir_notify_reparent(\"packed-refs\", &refs->path);\n+\tchdir_notify_register(NULL, packed_ref_store_reparent, refs);\n \treturn ref_store;\n }\n \n@@ -274,6 +287,7 @@ static void packed_ref_store_release(struct ref_store *ref_store)\n \tclear_snapshot(refs);\n \trollback_lock_file(&refs->lock);\n \tdelete_tempfile(&refs->tempfile);\n+\tchdir_notify_unregister(NULL, packed_ref_store_reparent, refs);\n \tfree(refs->path);\n }\n \ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 4ae22922de..8c93070677 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -365,6 +365,19 @@ static int reftable_be_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static void reftable_be_reparent(const char *name UNUSED,\n+\t\t\t\t const char *old_cwd,\n+\t\t\t\t const char *new_cwd,\n+\t\t\t\t void *payload)\n+{\n+\tstruct reftable_ref_store *refs = payload;\n+\tchar *tmp;\n+\n+\ttmp = reparent_relative_path(old_cwd, new_cwd, refs->base.gitdir);\n+\tfree(refs->base.gitdir);\n+\trefs->base.gitdir = tmp;\n+}\n+\n static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\t\t\t  const char *payload,\n \t\t\t\t\t  const char *gitdir,\n@@ -447,7 +460,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\t\tgoto done;\n \t}\n \n-\tchdir_notify_reparent(\"reftables-backend $GIT_DIR\", &refs->base.gitdir);\n+\tchdir_notify_register(NULL, reftable_be_reparent, refs);\n \n done:\n \tassert(refs->err != REFTABLE_API_ERROR);\n@@ -474,6 +487,7 @@ static void reftable_be_release(struct ref_store *ref_store)\n \t\tfree(be);\n \t}\n \tstrmap_clear(&refs->worktree_backends, 0);\n+\tchdir_notify_unregister(NULL, reftable_be_reparent, refs);\n }\n \n static int reftable_be_create_on_disk(struct ref_store *ref_store,\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546128","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-5-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 05/11] chdir-notify: drop unused `chdir_notify_reparent()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:27Z","receivedAt":"2026-06-22T08:28:43Z","isPatch":true,"body":"With the preceding commit we've removed all callers of\n`chdir_notify_reparent()`, so the function is unused now. Drop it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n chdir-notify.c | 26 --------------------------\n chdir-notify.h |  6 +-----\n 2 files changed, 1 insertion(+), 31 deletions(-)\n\ndiff --git a/chdir-notify.c b/chdir-notify.c\nindex f8bfe3cbef..1237a45e2e 100644\n--- a/chdir-notify.c\n+++ b/chdir-notify.c\n@@ -43,32 +43,6 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t}\n }\n \n-static void reparent_cb(const char *name,\n-\t\t\tconst char *old_cwd,\n-\t\t\tconst char *new_cwd,\n-\t\t\tvoid *data)\n-{\n-\tchar **path = data;\n-\tchar *tmp = *path;\n-\n-\tif (!tmp)\n-\t\treturn;\n-\n-\t*path = reparent_relative_path(old_cwd, new_cwd, tmp);\n-\tfree(tmp);\n-\n-\tif (name) {\n-\t\ttrace_printf_key(&trace_setup_key,\n-\t\t\t\t \"setup: reparent %s to '%s'\",\n-\t\t\t\t name, *path);\n-\t}\n-}\n-\n-void chdir_notify_reparent(const char *name, char **path)\n-{\n-\tchdir_notify_register(name, reparent_cb, path);\n-}\n-\n int chdir_notify(const char *new_cwd)\n {\n \tstruct strbuf old_cwd = STRBUF_INIT;\ndiff --git a/chdir-notify.h b/chdir-notify.h\nindex 81eb69d846..36b4114472 100644\n--- a/chdir-notify.h\n+++ b/chdir-notify.h\n@@ -19,10 +19,7 @@\n  *   chdir_notify_register(\"description\", foo, data);\n  *\n  * In practice most callers will want to move a relative path to the new root;\n- * they can use the reparent_relative_path() helper for that. If that's all\n- * you're doing, you can also use the convenience function:\n- *\n- *   chdir_notify_reparent(\"description\", &my_path);\n+ * they can use the reparent_relative_path() helper for that.\n  *\n  * Whenever a chdir event occurs, that will update my_path (if it's relative)\n  * to adjust for the new cwd by freeing any existing string and allocating a\n@@ -43,7 +40,6 @@ typedef void (*chdir_notify_callback)(const char *name,\n void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data);\n void chdir_notify_unregister(const char *name, chdir_notify_callback cb,\n \t\t\t     void *data);\n-void chdir_notify_reparent(const char *name, char **path);\n \n /*\n  *\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546129","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-6-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 06/11] repository: free main reference database","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:28Z","receivedAt":"2026-06-22T08:28:45Z","isPatch":true,"body":"While we release worktree and submodule reference databases when\nclearing a repository, we don't ever release the main reference\ndatabase. This memory leak went unnoticed because its pointer is\nkept alive by the \"chdir_notify\" subsystem.\n\nFix the memory leak.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n repository.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/repository.c b/repository.c\nindex 187dd471c4..e2b5c6712b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -421,6 +421,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->refs_private) {\n+\t\tref_store_release(repo->refs_private);\n+\t\tFREE_AND_NULL(repo->refs_private);\n+\t}\n+\n \tstrmap_for_each_entry(&repo->submodule_ref_stores, &iter, e)\n \t\tref_store_release(e->value);\n \tstrmap_clear(&repo->submodule_ref_stores, 1);\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546130","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-7-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 07/11] refs: move parsing of \"core.logAllRefUpdates\" back into ref stores","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:29Z","receivedAt":"2026-06-22T08:28:48Z","isPatch":true,"body":"In cc42c88945 (refs: extract out reflog config to generic layer,\n2026-05-04) we have refactored how we parse \"core.logAllRefUpdates\" so\nthat it happens in the generic layer. Unfortunately, this has worsened a\npreexisting issue where we may recurse when creating the reference store\nbecause of a chicken-and-egg problem between parsing the configuration\nand evaluating \"onbranch\" conditions.\n\nPrepare for a fix by essentially reverting that change so that we handle\nthis setting in the respective backends again. The backends are already\nparsing other configuration anyway, so by moving the logic back in there\nwe can ensure that all backend configuration is parsed the same way.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c      |  7 +++++--\n refs.c                  | 10 +++++++++-\n refs.h                  |  9 +++++++++\n refs/files-backend.c    | 20 +++++++++++++++++---\n refs/refs-internal.h    |  6 ------\n refs/reftable-backend.c | 20 +++++++++++---------\n repo-settings.c         | 16 ----------------\n repo-settings.h         |  9 ---------\n setup.c                 |  7 ++++++-\n 9 files changed, 57 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex b78b3a1d16..aee84ca897 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -952,10 +952,13 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \tconst char *old_desc, *reflog_msg;\n \tif (opts->new_branch) {\n \t\tif (opts->new_orphan_branch) {\n-\t\t\tenum log_refs_config log_all_ref_updates =\n-\t\t\t\trepo_settings_get_log_all_ref_updates(the_repository);\n+\t\t\tenum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;\n+\t\t\tconst char *value;\n \t\t\tchar *refname;\n \n+\t\t\tif (!repo_config_get_string_tmp(the_repository, \"core.logallrefupdates\", &value))\n+\t\t\t\tlog_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\n \t\t\trefname = mkpathdup(\"refs/heads/%s\", opts->new_orphan_branch);\n \t\t\tif (opts->new_branch_log &&\n \t\t\t    !should_autocreate_reflog(log_all_ref_updates, refname)) {\ndiff --git a/refs.c b/refs.c\nindex d3caa9a633..5b773b1c15 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1053,6 +1053,15 @@ static char *normalize_reflog_message(const char *msg)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n+enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value)\n+{\n+\tif (value && !strcasecmp(value, \"always\"))\n+\t\treturn LOG_REFS_ALWAYS;\n+\telse if (git_config_bool(\"core.logallrefupdates\", value))\n+\t\treturn LOG_REFS_NORMAL;\n+\treturn LOG_REFS_NONE;\n+}\n+\n int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\n \t\t\t     const char *refname)\n {\n@@ -2327,7 +2336,6 @@ static struct ref_store *ref_store_init(struct repository *repo,\n \tstruct ref_store *refs;\n \tstruct ref_store_init_options opts = {\n \t\t.access_flags = flags,\n-\t\t.log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo),\n \t};\n \n \tbe = find_ref_storage_backend(format);\ndiff --git a/refs.h b/refs.h\nindex 71d5c186d0..a381022c77 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -146,6 +146,15 @@ enum ref_transaction_error refs_verify_refname_available(struct ref_store *refs,\n \n int refs_ref_exists(struct ref_store *refs, const char *refname);\n \n+enum log_refs_config {\n+\tLOG_REFS_UNSET = -1,\n+\tLOG_REFS_NONE = 0,\n+\tLOG_REFS_NORMAL,\n+\tLOG_REFS_ALWAYS\n+};\n+\n+enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value);\n+\n int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\n \t\t\t     const char *refname);\n \ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 296981584b..79fb6735e1 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -117,6 +117,21 @@ static void files_ref_store_reparent(const char *name UNUSED,\n \trefs->gitcommondir = tmp;\n }\n \n+static int files_ref_store_config(const char *var, const char *value,\n+\t\t\t\t  const struct config_context *ctx UNUSED,\n+\t\t\t\t  void *payload)\n+{\n+\tstruct files_ref_store *refs = payload;\n+\n+\tif (!strcmp(var, \"core.prefersymlinkrefs\")) {\n+\t\trefs->prefer_symlink_refs = git_config_bool(var, value);\n+\t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n+\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\t}\n+\n+\treturn 0;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -141,10 +156,9 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \trefs->packed_ref_store =\n \t\tpacked_ref_store_init(repo, NULL, refs->gitcommondir, opts);\n \trefs->store_flags = opts->access_flags;\n-\trefs->log_all_ref_updates = opts->log_all_ref_updates;\n-\n-\trepo_config_get_bool(repo, \"core.prefersymlinkrefs\", &refs->prefer_symlink_refs);\n+\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n+\trepo_config(repo, files_ref_store_config, refs);\n \tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\ndiff --git a/refs/refs-internal.h b/refs/refs-internal.h\nindex a08d58900e..c3ac7b556f 100644\n--- a/refs/refs-internal.h\n+++ b/refs/refs-internal.h\n@@ -406,12 +406,6 @@ struct ref_store;\n struct ref_store_init_options {\n \t/* The kind of operations that the ref_store is allowed to perform. */\n \tunsigned int access_flags;\n-\n-\t/*\n-\t * Denotes under what conditions reflogs should be created when updating\n-\t * references.\n-\t */\n-\tenum log_refs_config log_all_ref_updates;\n };\n \n /*\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 8c93070677..5115a3f4ce 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -332,34 +332,36 @@ static void fill_reftable_log_record(struct reftable_log_record *log, const stru\n \n static int reftable_be_config(const char *var, const char *value,\n \t\t\t      const struct config_context *ctx,\n-\t\t\t      void *_opts)\n+\t\t\t      void *payload)\n {\n-\tstruct reftable_write_options *opts = _opts;\n+\tstruct reftable_ref_store *refs = payload;\n \n \tif (!strcmp(var, \"reftable.blocksize\")) {\n \t\tunsigned long block_size = git_config_ulong(var, value, ctx->kvi);\n \t\tif (block_size > 16777215)\n \t\t\tdie(\"reftable block size cannot exceed 16MB\");\n-\t\topts->block_size = block_size;\n+\t\trefs->write_options.block_size = block_size;\n \t} else if (!strcmp(var, \"reftable.restartinterval\")) {\n \t\tunsigned long restart_interval = git_config_ulong(var, value, ctx->kvi);\n \t\tif (restart_interval > UINT16_MAX)\n \t\t\tdie(\"reftable block size cannot exceed %u\", (unsigned)UINT16_MAX);\n-\t\topts->restart_interval = restart_interval;\n+\t\trefs->write_options.restart_interval = restart_interval;\n \t} else if (!strcmp(var, \"reftable.indexobjects\")) {\n-\t\topts->skip_index_objects = !git_config_bool(var, value);\n+\t\trefs->write_options.skip_index_objects = !git_config_bool(var, value);\n \t} else if (!strcmp(var, \"reftable.geometricfactor\")) {\n \t\tunsigned long factor = git_config_ulong(var, value, ctx->kvi);\n \t\tif (factor > UINT8_MAX)\n \t\t\tdie(\"reftable geometric factor cannot exceed %u\", (unsigned)UINT8_MAX);\n-\t\topts->auto_compaction_factor = factor;\n+\t\trefs->write_options.auto_compaction_factor = factor;\n \t} else if (!strcmp(var, \"reftable.locktimeout\")) {\n \t\tint64_t lock_timeout = git_config_int64(var, value, ctx->kvi);\n \t\tif (lock_timeout > LONG_MAX)\n \t\t\tdie(\"reftable lock timeout cannot exceed %\"PRIdMAX, (intmax_t)LONG_MAX);\n \t\tif (lock_timeout < 0 && lock_timeout != -1)\n \t\t\tdie(\"reftable lock timeout does not support negative values other than -1\");\n-\t\topts->lock_timeout_ms = lock_timeout;\n+\t\trefs->write_options.lock_timeout_ms = lock_timeout;\n+\t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n+\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n \t}\n \n \treturn 0;\n@@ -398,7 +400,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \n \tbase_ref_store_init(&refs->base, repo, refdir.buf, &refs_be_reftable);\n \tstrmap_init(&refs->worktree_backends);\n-\trefs->log_all_ref_updates = opts->log_all_ref_updates;\n \trefs->store_flags = opts->access_flags;\n \n \tswitch (repo->hash_algo->format_id) {\n@@ -415,8 +416,9 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \trefs->write_options.disable_auto_compact =\n \t\t!git_env_bool(\"GIT_TEST_REFTABLE_AUTOCOMPACTION\", 1);\n \trefs->write_options.lock_timeout_ms = 100;\n+\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n-\trepo_config(repo, reftable_be_config, &refs->write_options);\n+\trepo_config(repo, reftable_be_config, refs);\n \n \t/*\n \t * It is somewhat unfortunate that we have to mirror the default block\ndiff --git a/repo-settings.c b/repo-settings.c\nindex 208e09ff17..f3be3b8c5a 100644\n--- a/repo-settings.c\n+++ b/repo-settings.c\n@@ -177,22 +177,6 @@ void repo_settings_set_big_file_threshold(struct repository *repo, unsigned long\n \trepo->settings.big_file_threshold = value;\n }\n \n-enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo)\n-{\n-\tconst char *value;\n-\n-\tif (!repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value)) {\n-\t\tif (value && !strcasecmp(value, \"always\"))\n-\t\t\treturn LOG_REFS_ALWAYS;\n-\t\telse if (git_config_bool(\"core.logallrefupdates\", value))\n-\t\t\treturn LOG_REFS_NORMAL;\n-\t\telse\n-\t\t\treturn LOG_REFS_NONE;\n-\t}\n-\n-\treturn LOG_REFS_UNSET;\n-}\n-\n int repo_settings_get_warn_ambiguous_refs(struct repository *repo)\n {\n \tprepare_repo_settings(repo);\ndiff --git a/repo-settings.h b/repo-settings.h\nindex cad9c3f0cc..e5253ead02 100644\n--- a/repo-settings.h\n+++ b/repo-settings.h\n@@ -16,13 +16,6 @@ enum fetch_negotiation_setting {\n \tFETCH_NEGOTIATION_NOOP,\n };\n \n-enum log_refs_config {\n-\tLOG_REFS_UNSET = -1,\n-\tLOG_REFS_NONE = 0,\n-\tLOG_REFS_NORMAL,\n-\tLOG_REFS_ALWAYS\n-};\n-\n struct repo_settings {\n \tint initialized;\n \n@@ -86,8 +79,6 @@ struct repo_settings {\n void prepare_repo_settings(struct repository *r);\n void repo_settings_clear(struct repository *r);\n \n-/* Read the value for \"core.logAllRefUpdates\". */\n-enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo);\n /* Read the value for \"core.warnAmbiguousRefs\". */\n int repo_settings_get_warn_ambiguous_refs(struct repository *repo);\n /* Read the value for \"core.hooksPath\". */\ndiff --git a/setup.c b/setup.c\nindex 79125db565..0c6efb0560 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2584,10 +2584,15 @@ static int create_default_files(struct repository *repo,\n \tif (is_bare_repository())\n \t\trepo_config_set(repo, \"core.bare\", \"true\");\n \telse {\n+\t\tconst char *value;\n+\n \t\trepo_config_set(repo, \"core.bare\", \"false\");\n+\n \t\t/* allow template config file to override the default */\n-\t\tif (repo_settings_get_log_all_ref_updates(repo) == LOG_REFS_UNSET)\n+\t\tif (repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value) ||\n+\t\t    refs_parse_log_all_ref_updates_config(value) == LOG_REFS_UNSET)\n \t\t\trepo_config_set(repo, \"core.logallrefupdates\", \"true\");\n+\n \t\tif (needs_work_tree_config(original_git_dir, work_tree))\n \t\t\trepo_config_set(repo, \"core.worktree\", work_tree);\n \t}\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546131","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-8-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 08/11] refs/files: lazy-load configuration to fix chicken-and-egg","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:30Z","receivedAt":"2026-06-22T08:28:51Z","isPatch":true,"body":"When initializing the \"files\" reference backend we read the repository's\nconfig to parse \"core.preferSymlinkRefs\" and \"core.logAllRefUpdates\".\nThis results in a chicken-and-egg problem though, because parsing the\nconfiguration may require us to have access to the reference store\nalready when an \"onbranch\" condition exists.\n\nLuckily, all the configuration that we honor only relates to writing\nreferences. Consequently, we don't strictly need that configuration to\nbe readily available at initialization time, and we can easiliy defer\nparsing it to a later point in time.\n\nImplement this fix and add tests that verify that we can indeed properly\nparse these config knobs via an \"onbranch\" condition.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/files-backend.c        | 37 ++++++++++++++++++++++++++-----------\n t/t0600-reffiles-backend.sh | 21 +++++++++++++++++++++\n 2 files changed, 47 insertions(+), 11 deletions(-)\n\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 79fb6735e1..d0f379dcd6 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -84,12 +84,14 @@ struct files_ref_store {\n \tunsigned int store_flags;\n \n \tchar *gitcommondir;\n-\tenum log_refs_config log_all_ref_updates;\n-\tint prefer_symlink_refs;\n-\n \tstruct ref_cache *loose;\n-\n \tstruct ref_store *packed_ref_store;\n+\n+\tstruct files_ref_store_write_options {\n+\t\tenum log_refs_config log_all_ref_updates;\n+\t\tint prefer_symlink_refs;\n+\t\tbool initialized;\n+\t} write_opts_lazy_loaded;\n };\n \n static void clear_loose_ref_cache(struct files_ref_store *refs)\n@@ -121,17 +123,31 @@ static int files_ref_store_config(const char *var, const char *value,\n \t\t\t\t  const struct config_context *ctx UNUSED,\n \t\t\t\t  void *payload)\n {\n-\tstruct files_ref_store *refs = payload;\n+\tstruct files_ref_store_write_options *opts = payload;\n \n \tif (!strcmp(var, \"core.prefersymlinkrefs\")) {\n-\t\trefs->prefer_symlink_refs = git_config_bool(var, value);\n+\t\topts->prefer_symlink_refs = git_config_bool(var, value);\n \t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n-\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\t\topts->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n \t}\n \n \treturn 0;\n }\n \n+static const struct files_ref_store_write_options *files_ref_store_write_options(struct files_ref_store *refs)\n+{\n+\tstruct files_ref_store_write_options *opts = &refs->write_opts_lazy_loaded;\n+\n+\tif (opts->initialized)\n+\t\treturn opts;\n+\n+\topts->log_all_ref_updates = LOG_REFS_UNSET;\n+\trepo_config(refs->base.repo, files_ref_store_config, opts);\n+\n+\topts->initialized = true;\n+\treturn opts;\n+}\n+\n /*\n  * Create a new submodule ref cache and add it to the internal\n  * set of caches.\n@@ -156,9 +172,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n \trefs->packed_ref_store =\n \t\tpacked_ref_store_init(repo, NULL, refs->gitcommondir, opts);\n \trefs->store_flags = opts->access_flags;\n-\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n \n-\trepo_config(repo, files_ref_store_config, refs);\n \tchdir_notify_register(NULL, files_ref_store_reparent, refs);\n \n \tstrbuf_release(&refdir);\n@@ -1890,7 +1904,7 @@ static int log_ref_setup(struct files_ref_store *refs,\n \t\t\t const char *refname, int force_create,\n \t\t\t int *logfd, struct strbuf *err)\n {\n-\tenum log_refs_config log_refs_cfg = refs->log_all_ref_updates;\n+\tenum log_refs_config log_refs_cfg = files_ref_store_write_options(refs)->log_all_ref_updates;\n \tstruct strbuf logfile_sb = STRBUF_INIT;\n \tchar *logfile;\n \n@@ -3301,6 +3315,7 @@ static int files_transaction_finish(struct ref_store *ref_store,\n {\n \tstruct files_ref_store *refs =\n \t\tfiles_downcast(ref_store, 0, \"ref_transaction_finish\");\n+\tconst struct files_ref_store_write_options *write_opts = files_ref_store_write_options(refs);\n \tsize_t i;\n \tint ret = 0;\n \tstruct strbuf sb = STRBUF_INIT;\n@@ -3340,7 +3355,7 @@ static int files_transaction_finish(struct ref_store *ref_store,\n \t\t * We try creating a symlink, if that succeeds we continue to the\n \t\t * next update. If not, we try and create a regular symref.\n \t\t */\n-\t\tif (update->new_target && refs->prefer_symlink_refs)\n+\t\tif (update->new_target && write_opts->prefer_symlink_refs)\n \t\t\t/*\n \t\t\t * By using the `NOT_CONSTANT()` trick, we can avoid\n \t\t\t * errors by `clang`'s `-Wunreachable` logic that would\ndiff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh\nindex 74bfa2e9ba..bbbf6fa422 100755\n--- a/t/t0600-reffiles-backend.sh\n+++ b/t/t0600-reffiles-backend.sh\n@@ -519,4 +519,25 @@ test_expect_success 'symref transaction supports false symlink config' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success SYMLINKS,!MINGW,!WITH_BREAKING_CHANGES 'core.preferSymlinkRefs can be set up via onbranch condition' '\n+\ttest_when_finished \"git symbolic-ref -d TEST_SYMREF_HEAD\" &&\n+\ttest_when_finished \"rm -f .git/include\" &&\n+\tgit update-ref refs/heads/new @ &&\n+\tcat >.git/include <<-\\EOF &&\n+\t[core]\n+\t\tpreferSymlinkRefs = true\n+\tEOF\n+\ttest_config includeIf.onbranch:\"$(git branch --show-current)\".path \\\n+\t\t\"$(pwd)/.git/include\" &&\n+\tcat >stdin <<-EOF &&\n+\tstart\n+\tsymref-create TEST_SYMREF_HEAD refs/heads/new\n+\tprepare\n+\tcommit\n+\tEOF\n+\tgit update-ref --no-deref --stdin <stdin &&\n+\ttest_path_is_symlink .git/TEST_SYMREF_HEAD &&\n+\ttest \"$(test_readlink .git/TEST_SYMREF_HEAD)\" = refs/heads/new\n+'\n+\n test_done\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546133","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-9-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 09/11] reftable: split up write options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:31Z","receivedAt":"2026-06-22T08:28:53Z","isPatch":true,"body":"When initializing the reftable stack the caller may optionally pass some\nwrite options. These write options mix up two different concerns though:\n\n  - Of course, they allow the caller to configure how new reftables are\n    being written.\n\n  - But they also allow the caller to configure the stack itself, like\n    its hash ID and the `on_reload` callback.\n\nThis is somewhat awkward, as it doesn't easily give the caller the\nflexibility to for example write multiple reftables with different\noptions. Furthermore, this requires us to eagerly parse relevant\nconfiguration when initializing the reftable backend.\n\nRefactor the code by splitting out those options that configure the\nstack itself. Creating a new stack will thus only require this limited\nset of options, whereas the caller is expected to pass write options to\nall functions that end up writing tables.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/reftable-backend.c             |  29 +++---\n reftable/reftable-stack.h           |  30 +++++-\n reftable/reftable-writer.h          |  17 +---\n reftable/stack.c                    | 100 ++++++++++++-------\n reftable/stack.h                    |   2 +-\n reftable/writer.c                   |  21 ++--\n reftable/writer.h                   |   1 +\n t/helper/test-reftable.c            |   2 +-\n t/unit-tests/lib-reftable.c         |   8 +-\n t/unit-tests/lib-reftable.h         |   2 +\n t/unit-tests/u-reftable-merged.c    |   9 +-\n t/unit-tests/u-reftable-readwrite.c |  38 ++++++--\n t/unit-tests/u-reftable-stack.c     | 189 ++++++++++++++++--------------------\n t/unit-tests/u-reftable-table.c     |   8 +-\n 14 files changed, 258 insertions(+), 198 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 5115a3f4ce..608d71cf10 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -48,9 +48,9 @@ static void reftable_backend_on_reload(void *payload)\n \n static int reftable_backend_init(struct reftable_backend *be,\n \t\t\t\t const char *path,\n-\t\t\t\t const struct reftable_write_options *_opts)\n+\t\t\t\t const struct reftable_stack_options *_opts)\n {\n-\tstruct reftable_write_options opts = *_opts;\n+\tstruct reftable_stack_options opts = *_opts;\n \topts.on_reload = reftable_backend_on_reload;\n \topts.on_reload_payload = be;\n \treturn reftable_new_stack(&be->stack, path, &opts);\n@@ -140,6 +140,7 @@ struct reftable_ref_store {\n \t * is populated lazily when we try to resolve `worktrees/$worktree` refs.\n \t */\n \tstruct strmap worktree_backends;\n+\tstruct reftable_stack_options stack_options;\n \tstruct reftable_write_options write_options;\n \n \tunsigned int store_flags;\n@@ -190,7 +191,7 @@ static int backend_for_worktree(struct reftable_backend **out,\n \n \tCALLOC_ARRAY(*out, 1);\n \tstore->err = ret = reftable_backend_init(*out, worktree_dir.buf,\n-\t\t\t\t\t\t &store->write_options);\n+\t\t\t\t\t\t &store->stack_options);\n \tif (ret < 0) {\n \t\tfree(*out);\n \t\tgoto out;\n@@ -404,10 +405,10 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \n \tswitch (repo->hash_algo->format_id) {\n \tcase GIT_SHA1_FORMAT_ID:\n-\t\trefs->write_options.hash_id = REFTABLE_HASH_SHA1;\n+\t\trefs->stack_options.hash_id = REFTABLE_HASH_SHA1;\n \t\tbreak;\n \tcase GIT_SHA256_FORMAT_ID:\n-\t\trefs->write_options.hash_id = REFTABLE_HASH_SHA256;\n+\t\trefs->stack_options.hash_id = REFTABLE_HASH_SHA256;\n \t\tbreak;\n \tdefault:\n \t\tBUG(\"unknown hash algorithm %d\", repo->hash_algo->format_id);\n@@ -441,7 +442,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t}\n \tstrbuf_addstr(&path, \"/reftable\");\n \trefs->err = reftable_backend_init(&refs->main_backend, path.buf,\n-\t\t\t\t\t  &refs->write_options);\n+\t\t\t\t\t  &refs->stack_options);\n \tif (refs->err)\n \t\tgoto done;\n \n@@ -457,7 +458,7 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \t\tstrbuf_addstr(&refdir, \"/reftable\");\n \n \t\trefs->err = reftable_backend_init(&refs->worktree_backend, refdir.buf,\n-\t\t\t\t\t\t  &refs->write_options);\n+\t\t\t\t\t\t  &refs->stack_options);\n \t\tif (refs->err)\n \t\t\tgoto done;\n \t}\n@@ -997,6 +998,7 @@ static int prepare_transaction_update(struct write_transaction_table_arg **out,\n \t\tstruct reftable_addition *addition;\n \n \t\tret = reftable_stack_new_addition(&addition, be->stack,\n+\t\t\t\t\t\t  &refs->write_options,\n \t\t\t\t\t\t  REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \t\tif (ret) {\n \t\t\tif (ret == REFTABLE_LOCK_ERROR)\n@@ -1685,9 +1687,9 @@ static int reftable_be_optimize(struct ref_store *ref_store,\n \t\tstack = refs->main_backend.stack;\n \n \tif (opts->flags & REFS_OPTIMIZE_AUTO)\n-\t\tret = reftable_stack_auto_compact(stack);\n+\t\tret = reftable_stack_auto_compact(stack, &refs->write_options);\n \telse\n-\t\tret = reftable_stack_compact_all(stack, NULL);\n+\t\tret = reftable_stack_compact_all(stack, &refs->write_options, NULL);\n \tif (ret < 0) {\n \t\tret = error(_(\"unable to compact stack: %s\"),\n \t\t\t    reftable_error_str(ret));\n@@ -1721,8 +1723,8 @@ static int reftable_be_optimize_required(struct ref_store *ref_store,\n \tif (opts->flags & REFS_OPTIMIZE_AUTO)\n \t\tuse_heuristics = true;\n \n-\treturn reftable_stack_compaction_required(stack, use_heuristics,\n-\t\t\t\t\t\t  required);\n+\treturn reftable_stack_compaction_required(stack, &refs->write_options,\n+\t\t\t\t\t\t  use_heuristics, required);\n }\n \n struct write_create_symref_arg {\n@@ -1979,6 +1981,7 @@ static int reftable_be_rename_ref(struct ref_store *ref_store,\n \tif (ret)\n \t\tgoto done;\n \tret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg,\n+\t\t\t\t &refs->write_options,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2009,6 +2012,7 @@ static int reftable_be_copy_ref(struct ref_store *ref_store,\n \tif (ret)\n \t\tgoto done;\n \tret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg,\n+\t\t\t\t &refs->write_options,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2374,6 +2378,7 @@ static int reftable_be_create_reflog(struct ref_store *ref_store,\n \targ.stack = be->stack;\n \n \tret = reftable_stack_add(be->stack, &write_reflog_existence_table, &arg,\n+\t\t\t\t &refs->write_options,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2446,6 +2451,7 @@ static int reftable_be_delete_reflog(struct ref_store *ref_store,\n \targ.stack = be->stack;\n \n \tret = reftable_stack_add(be->stack, &write_reflog_delete_table, &arg,\n+\t\t\t\t &refs->write_options,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n \tassert(ret != REFTABLE_API_ERROR);\n@@ -2568,6 +2574,7 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store,\n \t\tgoto done;\n \n \tret = reftable_stack_new_addition(&add, be->stack,\n+\t\t\t\t\t  &refs->write_options,\n \t\t\t\t\t  REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \tif (ret < 0)\n \t\tgoto done;\ndiff --git a/reftable/reftable-stack.h b/reftable/reftable-stack.h\nindex 5f7be573fa..11f9963f4f 100644\n--- a/reftable/reftable-stack.h\n+++ b/reftable/reftable-stack.h\n@@ -26,11 +26,29 @@\n  */\n struct reftable_stack;\n \n+/* Options related to opening a stack. */\n+struct reftable_stack_options {\n+\t/*\n+\t * 4-byte identifier (\"sha1\", \"s256\") of the hash. Defaults to SHA1 if\n+\t * unset.\n+\t */\n+\tenum reftable_hash hash_id;\n+\n+\t/*\n+\t * Callback function to execute whenever the stack is being reloaded.\n+\t * This can be used e.g. to discard cached information that relies on\n+\t * the old stack's data. The payload data will be passed as argument to\n+\t * the callback.\n+\t */\n+\tvoid (*on_reload)(void *payload);\n+\tvoid *on_reload_payload;\n+};\n+\n /* open a new reftable stack. The tables along with the table list will be\n  *  stored in 'dir'. Typically, this should be .git/reftables.\n  */\n int reftable_new_stack(struct reftable_stack **dest, const char *dir,\n-\t\t       const struct reftable_write_options *opts);\n+\t\t       const struct reftable_stack_options *opts);\n \n /* returns the update_index at which a next table should be written. */\n uint64_t reftable_stack_next_update_index(struct reftable_stack *st);\n@@ -52,6 +70,7 @@ enum {\n  */\n int reftable_stack_new_addition(struct reftable_addition **dest,\n \t\t\t\tstruct reftable_stack *st,\n+\t\t\t\tconst struct reftable_write_options *opts,\n \t\t\t\tunsigned int flags);\n \n /* Adds a reftable to transaction. */\n@@ -77,7 +96,9 @@ void reftable_addition_destroy(struct reftable_addition *add);\n int reftable_stack_add(struct reftable_stack *st,\n \t\t       int (*write_table)(struct reftable_writer *wr,\n \t\t\t\t\t  void *write_arg),\n-\t\t       void *write_arg, unsigned flags);\n+\t\t       void *write_arg,\n+\t\t       const struct reftable_write_options *opts,\n+\t\t       unsigned flags);\n \n struct reftable_iterator;\n \n@@ -122,6 +143,7 @@ struct reftable_log_expiry_config {\n /* compacts all reftables into a giant table. Expire reflog entries if config is\n  * non-NULL */\n int reftable_stack_compact_all(struct reftable_stack *st,\n+\t\t\t       const struct reftable_write_options *opts,\n \t\t\t       struct reftable_log_expiry_config *config);\n \n /*\n@@ -132,11 +154,13 @@ int reftable_stack_compact_all(struct reftable_stack *st,\n  * compacted to maintain geometric progression.\n  */\n int reftable_stack_compaction_required(struct reftable_stack *st,\n+\t\t\t\t       const struct reftable_write_options *opts,\n \t\t\t\t       bool use_heuristics,\n \t\t\t\t       bool *required);\n \n /* heuristically compact unbalanced table stack. */\n-int reftable_stack_auto_compact(struct reftable_stack *st);\n+int reftable_stack_auto_compact(struct reftable_stack *st,\n+\t\t\t\tconst struct reftable_write_options *opts);\n \n /* delete stale .ref tables. */\n int reftable_stack_clean(struct reftable_stack *st);\ndiff --git a/reftable/reftable-writer.h b/reftable/reftable-writer.h\nindex a66db415c8..6ff4ddfc60 100644\n--- a/reftable/reftable-writer.h\n+++ b/reftable/reftable-writer.h\n@@ -28,11 +28,6 @@ struct reftable_write_options {\n \t/* how often to write complete keys in each block. */\n \tuint16_t restart_interval;\n \n-\t/* 4-byte identifier (\"sha1\", \"s256\") of the hash.\n-\t * Defaults to SHA1 if unset\n-\t */\n-\tenum reftable_hash hash_id;\n-\n \t/* Default mode for creating files. If unset, use 0666 (+umask) */\n \tunsigned int default_permissions;\n \n@@ -60,15 +55,6 @@ struct reftable_write_options {\n \t * negative value will cause us to block indefinitely.\n \t */\n \tlong lock_timeout_ms;\n-\n-\t/*\n-\t * Callback function to execute whenever the stack is being reloaded.\n-\t * This can be used e.g. to discard cached information that relies on\n-\t * the old stack's data. The payload data will be passed as argument to\n-\t * the callback.\n-\t */\n-\tvoid (*on_reload)(void *payload);\n-\tvoid *on_reload_payload;\n };\n \n /* reftable_block_stats holds statistics for a single block type */\n@@ -114,7 +100,8 @@ struct reftable_writer;\n int reftable_writer_new(struct reftable_writer **out,\n \t\t\tssize_t (*writer_func)(void *, const void *, size_t),\n \t\t\tint (*flush_func)(void *),\n-\t\t\tvoid *writer_arg, const struct reftable_write_options *opts);\n+\t\t\tvoid *writer_arg, enum reftable_hash hash_id,\n+\t\t\tconst struct reftable_write_options *opts);\n \n /*\n  * Set the range of update indices for the records we will add. When writing a\ndiff --git a/reftable/stack.c b/reftable/stack.c\nindex 1fba96ddb3..ab12926708 100644\n--- a/reftable/stack.c\n+++ b/reftable/stack.c\n@@ -501,10 +501,10 @@ static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st,\n }\n \n int reftable_new_stack(struct reftable_stack **dest, const char *dir,\n-\t\t       const struct reftable_write_options *_opts)\n+\t\t       const struct reftable_stack_options *_opts)\n {\n \tstruct reftable_buf list_file_name = REFTABLE_BUF_INIT;\n-\tstruct reftable_write_options opts = { 0 };\n+\tstruct reftable_stack_options opts = { 0 };\n \tstruct reftable_stack *p;\n \tint err;\n \n@@ -629,6 +629,7 @@ int reftable_stack_reload(struct reftable_stack *st)\n struct reftable_addition {\n \tstruct reftable_flock tables_list_lock;\n \tstruct reftable_stack *stack;\n+\tstruct reftable_write_options opts;\n \n \tchar **new_tables;\n \tsize_t new_tables_len, new_tables_cap;\n@@ -657,6 +658,7 @@ static void reftable_addition_close(struct reftable_addition *add)\n \n static int reftable_stack_init_addition(struct reftable_addition *add,\n \t\t\t\t\tstruct reftable_stack *st,\n+\t\t\t\t\tconst struct reftable_write_options *opts,\n \t\t\t\t\tunsigned int flags)\n {\n \tstruct reftable_buf lock_file_name = REFTABLE_BUF_INIT;\n@@ -664,15 +666,17 @@ static int reftable_stack_init_addition(struct reftable_addition *add,\n \n \tmemset(add, 0, sizeof(*add));\n \tadd->stack = st;\n+\tif (opts)\n+\t\tadd->opts = *opts;\n \n \terr = flock_acquire(&add->tables_list_lock, st->list_file,\n-\t\t\t    st->opts.lock_timeout_ms);\n+\t\t\t    add->opts.lock_timeout_ms);\n \tif (err < 0)\n \t\tgoto done;\n \n-\tif (st->opts.default_permissions) {\n+\tif (add->opts.default_permissions) {\n \t\tif (chmod(add->tables_list_lock.path,\n-\t\t\t  st->opts.default_permissions) < 0) {\n+\t\t\t  add->opts.default_permissions) < 0) {\n \t\t\terr = REFTABLE_IO_ERROR;\n \t\t\tgoto done;\n \t\t}\n@@ -702,12 +706,14 @@ static int reftable_stack_init_addition(struct reftable_addition *add,\n static int stack_try_add(struct reftable_stack *st,\n \t\t\t int (*write_table)(struct reftable_writer *wr,\n \t\t\t\t\t    void *arg),\n-\t\t\t void *arg, unsigned flags)\n+\t\t\t void *arg,\n+\t\t\t const struct reftable_write_options *opts,\n+\t\t\t unsigned flags)\n {\n \tstruct reftable_addition add;\n \tint err;\n \n-\terr = reftable_stack_init_addition(&add, st, flags);\n+\terr = reftable_stack_init_addition(&add, st, opts, flags);\n \tif (err < 0)\n \t\tgoto done;\n \n@@ -723,9 +729,11 @@ static int stack_try_add(struct reftable_stack *st,\n \n int reftable_stack_add(struct reftable_stack *st,\n \t\t       int (*write)(struct reftable_writer *wr, void *arg),\n-\t\t       void *arg, unsigned flags)\n+\t\t       void *arg,\n+\t\t       const struct reftable_write_options *opts,\n+\t\t       unsigned flags)\n {\n-\tint err = stack_try_add(st, write, arg, flags);\n+\tint err = stack_try_add(st, write, arg, opts, flags);\n \tif (err < 0) {\n \t\tif (err == REFTABLE_OUTDATED_ERROR) {\n \t\t\t/* Ignore error return, we want to propagate\n@@ -810,7 +818,7 @@ int reftable_addition_commit(struct reftable_addition *add)\n \tif (err)\n \t\tgoto done;\n \n-\tif (!add->stack->opts.disable_auto_compact) {\n+\tif (!add->opts.disable_auto_compact) {\n \t\t/*\n \t\t * Auto-compact the stack to keep the number of tables in\n \t\t * control. It is possible that a concurrent writer is already\n@@ -820,7 +828,7 @@ int reftable_addition_commit(struct reftable_addition *add)\n \t\t * concurrent writer, which causes `REFTABLE_OUTDATED_ERROR`.\n \t\t * Both of these errors are benign, so we simply ignore them.\n \t\t */\n-\t\terr = reftable_stack_auto_compact(add->stack);\n+\t\terr = reftable_stack_auto_compact(add->stack, &add->opts);\n \t\tif (err < 0 && err != REFTABLE_LOCK_ERROR &&\n \t\t    err != REFTABLE_OUTDATED_ERROR)\n \t\t\tgoto done;\n@@ -834,6 +842,7 @@ int reftable_addition_commit(struct reftable_addition *add)\n \n int reftable_stack_new_addition(struct reftable_addition **dest,\n \t\t\t\tstruct reftable_stack *st,\n+\t\t\t\tconst struct reftable_write_options *opts,\n \t\t\t\tunsigned int flags)\n {\n \tint err;\n@@ -842,7 +851,7 @@ int reftable_stack_new_addition(struct reftable_addition **dest,\n \tif (!*dest)\n \t\treturn REFTABLE_OUT_OF_MEMORY_ERROR;\n \n-\terr = reftable_stack_init_addition(*dest, st, flags);\n+\terr = reftable_stack_init_addition(*dest, st, opts, flags);\n \tif (err) {\n \t\treftable_free(*dest);\n \t\t*dest = NULL;\n@@ -862,7 +871,7 @@ int reftable_addition_add(struct reftable_addition *add,\n \tstruct reftable_writer *wr = NULL;\n \tstruct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT;\n \tstruct fd_writer writer = {\n-\t\t.opts = &add->stack->opts,\n+\t\t.opts = &add->opts,\n \t};\n \tint err = 0;\n \n@@ -883,9 +892,9 @@ int reftable_addition_add(struct reftable_addition *add,\n \terr = tmpfile_from_pattern(&tab_file, temp_tab_file_name.buf);\n \tif (err < 0)\n \t\tgoto done;\n-\tif (add->stack->opts.default_permissions) {\n+\tif (add->opts.default_permissions) {\n \t\tif (chmod(tab_file.path,\n-\t\t\t  add->stack->opts.default_permissions)) {\n+\t\t\t  add->opts.default_permissions)) {\n \t\t\terr = REFTABLE_IO_ERROR;\n \t\t\tgoto done;\n \t\t}\n@@ -893,7 +902,7 @@ int reftable_addition_add(struct reftable_addition *add,\n \n \twriter.fd = tab_file.fd;\n \terr = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush,\n-\t\t\t\t  &writer, &add->stack->opts);\n+\t\t\t\t  &writer, add->stack->opts.hash_id, &add->opts);\n \tif (err < 0)\n \t\tgoto done;\n \n@@ -1066,13 +1075,14 @@ static int stack_write_compact(struct reftable_stack *st,\n static int stack_compact_locked(struct reftable_stack *st,\n \t\t\t\tsize_t first, size_t last,\n \t\t\t\tstruct reftable_log_expiry_config *config,\n+\t\t\t\tconst struct reftable_write_options *opts,\n \t\t\t\tstruct reftable_tmpfile *tab_file_out)\n {\n \tstruct reftable_buf next_name = REFTABLE_BUF_INIT;\n \tstruct reftable_buf tab_file_path = REFTABLE_BUF_INIT;\n \tstruct reftable_writer *wr = NULL;\n \tstruct fd_writer writer=  {\n-\t\t.opts = &st->opts,\n+\t\t.opts = opts,\n \t};\n \tstruct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT;\n \tint err = 0;\n@@ -1094,15 +1104,15 @@ static int stack_compact_locked(struct reftable_stack *st,\n \tif (err < 0)\n \t\tgoto done;\n \n-\tif (st->opts.default_permissions &&\n-\t    chmod(tab_file.path, st->opts.default_permissions) < 0) {\n+\tif (opts->default_permissions &&\n+\t    chmod(tab_file.path, opts->default_permissions) < 0) {\n \t\terr = REFTABLE_IO_ERROR;\n \t\tgoto done;\n \t}\n \n \twriter.fd = tab_file.fd;\n \terr = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush,\n-\t\t\t\t  &writer, &st->opts);\n+\t\t\t\t  &writer, st->opts.hash_id, opts);\n \tif (err < 0)\n \t\tgoto done;\n \n@@ -1150,6 +1160,7 @@ enum stack_compact_range_flags {\n static int stack_compact_range(struct reftable_stack *st,\n \t\t\t       size_t first, size_t last,\n \t\t\t       struct reftable_log_expiry_config *expiry,\n+\t\t\t       const struct reftable_write_options *opts,\n \t\t\t       unsigned int flags)\n {\n \tstruct reftable_buf tables_list_buf = REFTABLE_BUF_INIT;\n@@ -1175,7 +1186,7 @@ static int stack_compact_range(struct reftable_stack *st,\n \t * Hold the lock so that we can read \"tables.list\" and lock all tables\n \t * which are part of the user-specified range.\n \t */\n-\terr = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms);\n+\terr = flock_acquire(&tables_list_lock, st->list_file, opts->lock_timeout_ms);\n \tif (err < 0)\n \t\tgoto done;\n \n@@ -1274,7 +1285,7 @@ static int stack_compact_range(struct reftable_stack *st,\n \t * these tables may end up with an empty new table in case tombstones\n \t * end up cancelling out all refs in that range.\n \t */\n-\terr = stack_compact_locked(st, first, last, expiry, &new_table);\n+\terr = stack_compact_locked(st, first, last, expiry, opts, &new_table);\n \tif (err < 0) {\n \t\tif (err != REFTABLE_EMPTY_TABLE_ERROR)\n \t\t\tgoto done;\n@@ -1286,13 +1297,13 @@ static int stack_compact_range(struct reftable_stack *st,\n \t * \"tables.list\". We'll then replace the compacted range of tables with\n \t * the new table.\n \t */\n-\terr = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms);\n+\terr = flock_acquire(&tables_list_lock, st->list_file, opts->lock_timeout_ms);\n \tif (err < 0)\n \t\tgoto done;\n \n-\tif (st->opts.default_permissions) {\n+\tif (opts->default_permissions) {\n \t\tif (chmod(tables_list_lock.path,\n-\t\t\t  st->opts.default_permissions) < 0) {\n+\t\t\t  opts->default_permissions) < 0) {\n \t\t\terr = REFTABLE_IO_ERROR;\n \t\t\tgoto done;\n \t\t}\n@@ -1513,10 +1524,16 @@ static int stack_compact_range(struct reftable_stack *st,\n }\n \n int reftable_stack_compact_all(struct reftable_stack *st,\n+\t\t\t       const struct reftable_write_options *opts,\n \t\t\t       struct reftable_log_expiry_config *config)\n {\n+\tstruct reftable_write_options opts_default = { 0 };\n \tsize_t last = st->merged->tables_len ? st->merged->tables_len - 1 : 0;\n-\treturn stack_compact_range(st, 0, last, config, 0);\n+\n+\tif (!opts)\n+\t\topts = &opts_default;\n+\n+\treturn stack_compact_range(st, 0, last, config, opts, 0);\n }\n \n static int segment_size(struct segment *s)\n@@ -1601,6 +1618,7 @@ struct segment suggest_compaction_segment(uint64_t *sizes, size_t n,\n }\n \n static int stack_segments_for_compaction(struct reftable_stack *st,\n+\t\t\t\t\t const struct reftable_write_options *opts,\n \t\t\t\t\t struct segment *seg)\n {\n \tint version = (st->opts.hash_id == REFTABLE_HASH_SHA1) ? 1 : 2;\n@@ -1615,13 +1633,14 @@ static int stack_segments_for_compaction(struct reftable_stack *st,\n \t\tsizes[i] = st->tables[i]->size - overhead;\n \n \t*seg = suggest_compaction_segment(sizes, st->merged->tables_len,\n-\t\t\t\t\t  st->opts.auto_compaction_factor);\n+\t\t\t\t\t  opts->auto_compaction_factor);\n \treftable_free(sizes);\n \n \treturn 0;\n }\n \n static int update_segment_if_compaction_required(struct reftable_stack *st,\n+\t\t\t\t\t\t const struct reftable_write_options *opts,\n \t\t\t\t\t\t struct segment *seg,\n \t\t\t\t\t\t bool use_geometric,\n \t\t\t\t\t\t bool *required)\n@@ -1638,7 +1657,7 @@ static int update_segment_if_compaction_required(struct reftable_stack *st,\n \t\treturn 0;\n \t}\n \n-\terr = stack_segments_for_compaction(st, seg);\n+\terr = stack_segments_for_compaction(st, opts, seg);\n \tif (err)\n \t\treturn err;\n \n@@ -1647,27 +1666,40 @@ static int update_segment_if_compaction_required(struct reftable_stack *st,\n }\n \n int reftable_stack_compaction_required(struct reftable_stack *st,\n+\t\t\t\t       const struct reftable_write_options *opts,\n \t\t\t\t       bool use_heuristics,\n \t\t\t\t       bool *required)\n {\n+\tstruct reftable_write_options opts_default = { 0 };\n \tstruct segment seg;\n-\treturn update_segment_if_compaction_required(st, &seg, use_heuristics,\n-\t\t\t\t\t\t     required);\n+\n+\tif (!opts)\n+\t\topts = &opts_default;\n+\n+\treturn update_segment_if_compaction_required(st, opts, &seg,\n+\t\t\t\t\t\t     use_heuristics, required);\n }\n \n-int reftable_stack_auto_compact(struct reftable_stack *st)\n+int reftable_stack_auto_compact(struct reftable_stack *st,\n+\t\t\t\tconst struct reftable_write_options *opts)\n {\n+\tstruct reftable_write_options opts_default = { 0 };\n \tstruct segment seg;\n \tbool required;\n \tint err;\n \n-\terr = update_segment_if_compaction_required(st, &seg, true, &required);\n+\tif (!opts)\n+\t\topts = &opts_default;\n+\n+\terr = update_segment_if_compaction_required(st, opts, &seg, true,\n+\t\t\t\t\t\t    &required);\n \tif (err)\n \t\treturn err;\n \n \tif (required)\n \t\treturn stack_compact_range(st, seg.start, seg.end - 1,\n-\t\t\t\t\t   NULL, STACK_COMPACT_RANGE_BEST_EFFORT);\n+\t\t\t\t\t   NULL, opts,\n+\t\t\t\t\t   STACK_COMPACT_RANGE_BEST_EFFORT);\n \n \treturn 0;\n }\n@@ -1807,7 +1839,7 @@ static int reftable_stack_clean_locked(struct reftable_stack *st)\n int reftable_stack_clean(struct reftable_stack *st)\n {\n \tstruct reftable_addition *add = NULL;\n-\tint err = reftable_stack_new_addition(&add, st, 0);\n+\tint err = reftable_stack_new_addition(&add, st, NULL, 0);\n \tif (err < 0) {\n \t\tgoto done;\n \t}\ndiff --git a/reftable/stack.h b/reftable/stack.h\nindex bc28f2998a..f7901e6c6f 100644\n--- a/reftable/stack.h\n+++ b/reftable/stack.h\n@@ -20,7 +20,7 @@ struct reftable_stack {\n \n \tchar *reftable_dir;\n \n-\tstruct reftable_write_options opts;\n+\tstruct reftable_stack_options opts;\n \n \tstruct reftable_table **tables;\n \tsize_t tables_len;\ndiff --git a/reftable/writer.c b/reftable/writer.c\nindex 0133b64975..f850e9d599 100644\n--- a/reftable/writer.c\n+++ b/reftable/writer.c\n@@ -80,9 +80,6 @@ static void options_set_defaults(struct reftable_write_options *opts)\n \t\topts->restart_interval = 16;\n \t}\n \n-\tif (opts->hash_id == 0) {\n-\t\topts->hash_id = REFTABLE_HASH_SHA1;\n-\t}\n \tif (opts->block_size == 0) {\n \t\topts->block_size = DEFAULT_BLOCK_SIZE;\n \t}\n@@ -90,7 +87,7 @@ static void options_set_defaults(struct reftable_write_options *opts)\n \n static int writer_version(struct reftable_writer *w)\n {\n-\treturn (w->opts.hash_id == 0 || w->opts.hash_id == REFTABLE_HASH_SHA1) ?\n+\treturn (w->hash_id == 0 || w->hash_id == REFTABLE_HASH_SHA1) ?\n \t\t\t     1 :\n \t\t\t     2;\n }\n@@ -107,7 +104,7 @@ static int writer_write_header(struct reftable_writer *w, uint8_t *dest)\n \tif (writer_version(w) == 2) {\n \t\tuint32_t hash_id;\n \n-\t\tswitch (w->opts.hash_id) {\n+\t\tswitch (w->hash_id) {\n \t\tcase REFTABLE_HASH_SHA1:\n \t\t\thash_id = REFTABLE_FORMAT_ID_SHA1;\n \t\t\tbreak;\n@@ -134,7 +131,7 @@ static int writer_reinit_block_writer(struct reftable_writer *w, uint8_t typ)\n \treftable_buf_reset(&w->last_key);\n \tret = block_writer_init(&w->block_writer_data, typ, w->block,\n \t\t\t\tw->opts.block_size, block_start,\n-\t\t\t\thash_size(w->opts.hash_id));\n+\t\t\t\thash_size(w->hash_id));\n \tif (ret < 0)\n \t\treturn ret;\n \n@@ -147,7 +144,9 @@ static int writer_reinit_block_writer(struct reftable_writer *w, uint8_t typ)\n int reftable_writer_new(struct reftable_writer **out,\n \t\t\tssize_t (*writer_func)(void *, const void *, size_t),\n \t\t\tint (*flush_func)(void *),\n-\t\t\tvoid *writer_arg, const struct reftable_write_options *_opts)\n+\t\t\tvoid *writer_arg,\n+\t\t\tenum reftable_hash hash_id,\n+\t\t\tconst struct reftable_write_options *_opts)\n {\n \tstruct reftable_write_options opts = {0};\n \tstruct reftable_writer *wp;\n@@ -162,6 +161,9 @@ int reftable_writer_new(struct reftable_writer **out,\n \tif (opts.block_size >= (1 << 24))\n \t\treturn REFTABLE_API_ERROR;\n \n+\tif (!hash_id)\n+\t\thash_id = REFTABLE_HASH_SHA1;\n+\n \treftable_buf_init(&wp->block_writer_data.last_key);\n \treftable_buf_init(&wp->last_key);\n \treftable_buf_init(&wp->scratch);\n@@ -173,6 +175,7 @@ int reftable_writer_new(struct reftable_writer **out,\n \twp->write = writer_func;\n \twp->write_arg = writer_arg;\n \twp->opts = opts;\n+\twp->hash_id = hash_id;\n \twp->flush = flush_func;\n \twriter_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF);\n \n@@ -367,7 +370,7 @@ int reftable_writer_add_ref(struct reftable_writer *w,\n \tif (!w->opts.skip_index_objects && reftable_ref_record_val1(ref)) {\n \t\treftable_buf_reset(&w->scratch);\n \t\terr = reftable_buf_add(&w->scratch, (char *)reftable_ref_record_val1(ref),\n-\t\t\t\t       hash_size(w->opts.hash_id));\n+\t\t\t\t       hash_size(w->hash_id));\n \t\tif (err < 0)\n \t\t\tgoto out;\n \n@@ -379,7 +382,7 @@ int reftable_writer_add_ref(struct reftable_writer *w,\n \tif (!w->opts.skip_index_objects && reftable_ref_record_val2(ref)) {\n \t\treftable_buf_reset(&w->scratch);\n \t\terr = reftable_buf_add(&w->scratch, reftable_ref_record_val2(ref),\n-\t\t\t\t       hash_size(w->opts.hash_id));\n+\t\t\t\t       hash_size(w->hash_id));\n \t\tif (err < 0)\n \t\t\tgoto out;\n \ndiff --git a/reftable/writer.h b/reftable/writer.h\nindex 9f53610b27..c08fc413e1 100644\n--- a/reftable/writer.h\n+++ b/reftable/writer.h\n@@ -27,6 +27,7 @@ struct reftable_writer {\n \tuint64_t next;\n \tuint64_t min_update_index, max_update_index;\n \tstruct reftable_write_options opts;\n+\tenum reftable_hash hash_id;\n \n \t/* memory buffer for writing */\n \tuint8_t *block;\ndiff --git a/t/helper/test-reftable.c b/t/helper/test-reftable.c\nindex b16c0722c8..fc49fafc34 100644\n--- a/t/helper/test-reftable.c\n+++ b/t/helper/test-reftable.c\n@@ -174,7 +174,7 @@ static int dump_table(struct reftable_merged_table *mt)\n static int dump_stack(const char *stackdir, uint32_t hash_id)\n {\n \tstruct reftable_stack *stack = NULL;\n-\tstruct reftable_write_options opts = { .hash_id = hash_id };\n+\tstruct reftable_stack_options opts = { .hash_id = hash_id };\n \tstruct reftable_merged_table *merged = NULL;\n \n \tint err = reftable_new_stack(&stack, stackdir, &opts);\ndiff --git a/t/unit-tests/lib-reftable.c b/t/unit-tests/lib-reftable.c\nindex fdb5b11a20..19a3ac8b80 100644\n--- a/t/unit-tests/lib-reftable.c\n+++ b/t/unit-tests/lib-reftable.c\n@@ -25,11 +25,12 @@ static int strbuf_writer_flush(void *arg UNUSED)\n }\n \n struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,\n+\t\t\t\t\t\t enum reftable_hash hash_id,\n \t\t\t\t\t\t struct reftable_write_options *opts)\n {\n \tstruct reftable_writer *writer;\n \tint ret = reftable_writer_new(&writer, &strbuf_writer_write, &strbuf_writer_flush,\n-\t\t\t\t      buf, opts);\n+\t\t\t\t      buf, hash_id, opts);\n \tcl_assert(!ret);\n \treturn writer;\n }\n@@ -39,6 +40,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf,\n \t\t\t     size_t nrefs,\n \t\t\t     struct reftable_log_record *logs,\n \t\t\t     size_t nlogs,\n+\t\t\t     enum reftable_hash hash_id,\n \t\t\t     struct reftable_write_options *_opts)\n {\n \tstruct reftable_write_options opts = { 0 };\n@@ -66,7 +68,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf,\n \t\t\tmin = ui;\n \t}\n \n-\twriter = cl_reftable_strbuf_writer(buf, &opts);\n+\twriter = cl_reftable_strbuf_writer(buf, hash_id, &opts);\n \tret = reftable_writer_set_limits(writer, min, max);\n \tcl_assert(!ret);\n \n@@ -88,7 +90,7 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf,\n \t\tsize_t off = i * (opts.block_size ? opts.block_size\n \t\t\t\t\t\t  : DEFAULT_BLOCK_SIZE);\n \t\tif (!off)\n-\t\t\toff = header_size(opts.hash_id == REFTABLE_HASH_SHA256 ? 2 : 1);\n+\t\t\toff = header_size(hash_id == REFTABLE_HASH_SHA256 ? 2 : 1);\n \t\tcl_assert(buf->buf[off] == 'r');\n \t}\n \ndiff --git a/t/unit-tests/lib-reftable.h b/t/unit-tests/lib-reftable.h\nindex d7e6d3136f..caf443d147 100644\n--- a/t/unit-tests/lib-reftable.h\n+++ b/t/unit-tests/lib-reftable.h\n@@ -10,6 +10,7 @@ struct reftable_buf;\n void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);\n \n struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,\n+\t\t\t\t\t\t enum reftable_hash hash_id,\n \t\t\t\t\t\t struct reftable_write_options *opts);\n \n void cl_reftable_write_to_buf(struct reftable_buf *buf,\n@@ -17,4 +18,5 @@ void cl_reftable_write_to_buf(struct reftable_buf *buf,\n \t\t\t     size_t nrecords,\n \t\t\t     struct reftable_log_record *logs,\n \t\t\t     size_t nlogs,\n+\t\t\t     enum reftable_hash hash_id,\n \t\t\t     struct reftable_write_options *opts);\ndiff --git a/t/unit-tests/u-reftable-merged.c b/t/unit-tests/u-reftable-merged.c\nindex 54cb7fc2a7..21232c1e4f 100644\n--- a/t/unit-tests/u-reftable-merged.c\n+++ b/t/unit-tests/u-reftable-merged.c\n@@ -34,7 +34,8 @@ merged_table_from_records(struct reftable_ref_record **refs,\n \tcl_assert(*source != NULL);\n \n \tfor (size_t i = 0; i < n; i++) {\n-\t\tcl_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0, &opts);\n+\t\tcl_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0,\n+\t\t\t\t\t REFTABLE_HASH_SHA1, &opts);\n \t\tblock_source_from_buf(&(*source)[i], &buf[i]);\n \n \t\terr = reftable_table_new(&(*tables)[i], &(*source)[i],\n@@ -357,7 +358,8 @@ merged_table_from_log_records(struct reftable_log_record **logs,\n \tcl_assert(*source != NULL);\n \n \tfor (size_t i = 0; i < n; i++) {\n-\t\tcl_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i], &opts);\n+\t\tcl_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i],\n+\t\t\t\t\t REFTABLE_HASH_SHA1, &opts);\n \t\tblock_source_from_buf(&(*source)[i], &buf[i]);\n \n \t\terr = reftable_table_new(&(*tables)[i], &(*source)[i],\n@@ -487,7 +489,8 @@ void test_reftable_merged__default_write_opts(void)\n {\n \tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1, &opts);\n \tstruct reftable_ref_record rec = {\n \t\t.refname = (char *) \"master\",\n \t\t.update_index = 1,\ndiff --git a/t/unit-tests/u-reftable-readwrite.c b/t/unit-tests/u-reftable-readwrite.c\nindex 4d8c4be5f1..5794b460c6 100644\n--- a/t/unit-tests/u-reftable-readwrite.c\n+++ b/t/unit-tests/u-reftable-readwrite.c\n@@ -48,7 +48,6 @@ static void write_table(char ***names, struct reftable_buf *buf, int N,\n {\n \tstruct reftable_write_options opts = {\n \t\t.block_size = block_size,\n-\t\t.hash_id = hash_id,\n \t};\n \tstruct reftable_ref_record *refs;\n \tstruct reftable_log_record *logs;\n@@ -78,7 +77,7 @@ static void write_table(char ***names, struct reftable_buf *buf, int N,\n \t\tlogs[i].value.update.message = (char *) \"message\";\n \t}\n \n-\tcl_reftable_write_to_buf(buf, refs, N, logs, N, &opts);\n+\tcl_reftable_write_to_buf(buf, refs, N, logs, N, hash_id, &opts);\n \n \treftable_free(refs);\n \treftable_free(logs);\n@@ -103,6 +102,7 @@ void test_reftable_readwrite__log_buffer_size(void)\n \t\t\t\t\t   .message = (char *) \"commit: 9\\n\",\n \t\t\t\t   } } };\n \tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n \t\t\t\t\t\t\t      &opts);\n \n \t/* This tests buffer extension for log compression. Must use a random\n@@ -143,6 +143,7 @@ void test_reftable_readwrite__log_overflow(void)\n \t\t},\n \t};\n \tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n \t\t\t\t\t\t\t      &opts);\n \n \tmemset(msg, 'x', sizeof(msg) - 1);\n@@ -157,6 +158,7 @@ void test_reftable_readwrite__log_write_limits(void)\n \tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n \tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n \t\t\t\t\t\t\t      &opts);\n \tstruct reftable_log_record log = {\n \t\t.refname = (char *)\"refs/head/master\",\n@@ -202,7 +204,9 @@ void test_reftable_readwrite__log_write_read(void)\n \tstruct reftable_table *table;\n \tstruct reftable_block_source source = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tconst struct reftable_stats *stats = NULL;\n \tint N = 2, i;\n \tchar **names;\n@@ -299,6 +303,7 @@ void test_reftable_readwrite__log_zlib_corruption(void)\n \tstruct reftable_block_source source = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n \tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n \t\t\t\t\t\t\t      &opts);\n \tconst struct reftable_stats *stats = NULL;\n \tchar message[100] = { 0 };\n@@ -531,6 +536,7 @@ static void t_table_refs_for(int indexed)\n \tstruct reftable_block_source source = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n \tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n \t\t\t\t\t\t\t      &opts);\n \tstruct reftable_iterator it = { 0 };\n \tint N = 50, j, i;\n@@ -622,7 +628,9 @@ void test_reftable_readwrite__write_empty_table(void)\n {\n \tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tstruct reftable_block_source source = { 0 };\n \tstruct reftable_table *table = NULL;\n \tstruct reftable_ref_record rec = { 0 };\n@@ -660,7 +668,9 @@ void test_reftable_readwrite__write_object_id_min_length(void)\n \t\t.block_size = 75,\n \t};\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tstruct reftable_ref_record ref = {\n \t\t.update_index = 1,\n \t\t.value_type = REFTABLE_REF_VAL1,\n@@ -691,7 +701,9 @@ void test_reftable_readwrite__write_object_id_length(void)\n \t\t.block_size = 75,\n \t};\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tstruct reftable_ref_record ref = {\n \t\t.update_index = 1,\n \t\t.value_type = REFTABLE_REF_VAL1,\n@@ -721,7 +733,9 @@ void test_reftable_readwrite__write_empty_key(void)\n {\n \tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tstruct reftable_ref_record ref = {\n \t\t.refname = (char *) \"\",\n \t\t.update_index = 1,\n@@ -740,7 +754,9 @@ void test_reftable_readwrite__write_key_order(void)\n {\n \tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n-\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);\n+\tstruct reftable_writer *w = cl_reftable_strbuf_writer(&buf,\n+\t\t\t\t\t\t\t      REFTABLE_HASH_SHA1,\n+\t\t\t\t\t\t\t      &opts);\n \tstruct reftable_ref_record refs[2] = {\n \t\t{\n \t\t\t.refname = (char *) \"b\",\n@@ -787,7 +803,8 @@ void test_reftable_readwrite__write_multiple_indices(void)\n \tint i;\n \tint err;\n \n-\twriter = cl_reftable_strbuf_writer(&writer_buf, &opts);\n+\twriter = cl_reftable_strbuf_writer(&writer_buf, REFTABLE_HASH_SHA1,\n+\t\t\t\t\t   &opts);\n \treftable_writer_set_limits(writer, 1, 1);\n \tfor (i = 0; i < 100; i++) {\n \t\tstruct reftable_ref_record ref = {\n@@ -861,7 +878,8 @@ void test_reftable_readwrite__write_multi_level_index(void)\n \tstruct reftable_table *table;\n \tint err;\n \n-\twriter = cl_reftable_strbuf_writer(&writer_buf, &opts);\n+\twriter = cl_reftable_strbuf_writer(&writer_buf, REFTABLE_HASH_SHA1,\n+\t\t\t\t\t   &opts);\n \treftable_writer_set_limits(writer, 1, 1);\n \tfor (size_t i = 0; i < 200; i++) {\n \t\tstruct reftable_ref_record ref = {\ndiff --git a/t/unit-tests/u-reftable-stack.c b/t/unit-tests/u-reftable-stack.c\nindex b8110cdeee..e6c1635940 100644\n--- a/t/unit-tests/u-reftable-stack.c\n+++ b/t/unit-tests/u-reftable-stack.c\n@@ -111,10 +111,9 @@ static int write_test_ref(struct reftable_writer *wr, void *arg)\n static void write_n_ref_tables(struct reftable_stack *st,\n \t\t\t       size_t n)\n {\n-\tint disable_auto_compact;\n-\n-\tdisable_auto_compact = st->opts.disable_auto_compact;\n-\tst->opts.disable_auto_compact = 1;\n+\tstruct reftable_write_options opts = {\n+\t\t.disable_auto_compact = 1,\n+\t};\n \n \tfor (size_t i = 0; i < n; i++) {\n \t\tstruct reftable_ref_record ref = {\n@@ -128,10 +127,8 @@ static void write_n_ref_tables(struct reftable_stack *st,\n \t\tcl_reftable_set_hash(ref.value.val1, i, REFTABLE_HASH_SHA1);\n \n \t\tcl_assert_equal_i(reftable_stack_add(st,\n-\t\t\t\t\t\t     &write_test_ref, &ref, 0), 0);\n+\t\t\t\t\t\t     &write_test_ref, &ref, &opts, 0), 0);\n \t}\n-\n-\tst->opts.disable_auto_compact = disable_auto_compact;\n }\n \n struct write_log_arg {\n@@ -168,10 +165,10 @@ void test_reftable_stack__add_one(void)\n \tstruct stat stat_result = { 0 };\n \tint err;\n \n-\terr = reftable_new_stack(&st, dir, &opts);\n+\terr = reftable_new_stack(&st, dir, NULL);\n \tcl_assert(!err);\n \n-\terr = reftable_stack_add(st, write_test_ref, &ref, 0);\n+\terr = reftable_stack_add(st, write_test_ref, &ref, &opts, 0);\n \tcl_assert(!err);\n \n \terr = reftable_stack_read_ref(st, ref.refname, &dest);\n@@ -210,7 +207,6 @@ void test_reftable_stack__add_one(void)\n \n void test_reftable_stack__uptodate(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st1 = NULL;\n \tstruct reftable_stack *st2 = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n@@ -232,15 +228,15 @@ void test_reftable_stack__uptodate(void)\n \t/* simulate multi-process access to the same stack\n \t   by creating two stacks for the same directory.\n \t */\n-\tcl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0);\n-\tcl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_add(st1, write_test_ref,\n-\t\t\t\t\t     &ref1, 0), 0);\n+\t\t\t\t\t     &ref1, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_stack_add(st2, write_test_ref,\n-\t\t\t\t\t     &ref2, 0), REFTABLE_OUTDATED_ERROR);\n+\t\t\t\t\t     &ref2, NULL, 0), REFTABLE_OUTDATED_ERROR);\n \tcl_assert_equal_i(reftable_stack_reload(st2), 0);\n \tcl_assert_equal_i(reftable_stack_add(st2, write_test_ref,\n-\t\t\t\t\t     &ref2, 0), 0);\n+\t\t\t\t\t     &ref2, NULL, 0), 0);\n \treftable_stack_destroy(st1);\n \treftable_stack_destroy(st2);\n \tclear_dir(dir);\n@@ -249,7 +245,6 @@ void test_reftable_stack__uptodate(void)\n void test_reftable_stack__transaction_api(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tstruct reftable_addition *add = NULL;\n \n@@ -261,11 +256,11 @@ void test_reftable_stack__transaction_api(void)\n \t};\n \tstruct reftable_ref_record dest = { 0 };\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \treftable_addition_destroy(add);\n \n-\tcl_assert_equal_i(reftable_stack_new_addition(&add, st, 0), 0);\n+\tcl_assert_equal_i(reftable_stack_new_addition(&add, st, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_addition_add(add, write_test_ref,\n \t\t\t\t\t\t&ref), 0);\n \tcl_assert_equal_i(reftable_addition_commit(add), 0);\n@@ -306,7 +301,7 @@ void test_reftable_stack__transaction_with_reload(void)\n \n \tcl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0);\n-\tcl_assert_equal_i(reftable_stack_new_addition(&add, st1, 0), 0);\n+\tcl_assert_equal_i(reftable_stack_new_addition(&add, st1, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_addition_add(add, write_test_ref,\n \t\t\t\t\t\t&refs[0]), 0);\n \tcl_assert_equal_i(reftable_addition_commit(add), 0);\n@@ -317,9 +312,9 @@ void test_reftable_stack__transaction_with_reload(void)\n \t * create the addition and lock the stack by default, but allow the\n \t * reload to happen when REFTABLE_STACK_NEW_ADDITION_RELOAD is set.\n \t */\n-\tcl_assert_equal_i(reftable_stack_new_addition(&add, st2, 0),\n+\tcl_assert_equal_i(reftable_stack_new_addition(&add, st2, NULL, 0),\n \t\t\t\t\t\t      REFTABLE_OUTDATED_ERROR);\n-\tcl_assert_equal_i(reftable_stack_new_addition(&add, st2,\n+\tcl_assert_equal_i(reftable_stack_new_addition(&add, st2, NULL,\n \t\t\t\t\t\t      REFTABLE_STACK_NEW_ADDITION_RELOAD), 0);\n \tcl_assert_equal_i(reftable_addition_add(add, write_test_ref,\n \t\t\t\t\t\t&refs[1]), 0);\n@@ -342,12 +337,11 @@ void test_reftable_stack__transaction_with_reload(void)\n void test_reftable_stack__transaction_api_performs_auto_compaction(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = {0};\n \tstruct reftable_addition *add = NULL;\n \tstruct reftable_stack *st = NULL;\n \tsize_t n = 20;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (size_t i = 0; i <= n; i++) {\n \t\tstruct reftable_ref_record ref = {\n@@ -356,6 +350,9 @@ void test_reftable_stack__transaction_api_performs_auto_compaction(void)\n \t\t\t.value.symref = (char *) \"master\",\n \t\t};\n \t\tchar name[100];\n+\t\tstruct reftable_write_options write_opts = {\n+\t\t\t.disable_auto_compact = (i != n),\n+\t\t};\n \n \t\tsnprintf(name, sizeof(name), \"branch%04\"PRIuMAX, (uintmax_t)i);\n \t\tref.refname = name;\n@@ -365,10 +362,8 @@ void test_reftable_stack__transaction_api_performs_auto_compaction(void)\n \t\t * we can ensure that we indeed honor this setting and have\n \t\t * better control over when exactly auto compaction runs.\n \t\t */\n-\t\tst->opts.disable_auto_compact = i != n;\n-\n \t\tcl_assert_equal_i(reftable_stack_new_addition(&add,\n-\t\t\t\t\t\t\t      st, 0), 0);\n+\t\t\t\t\t\t\t      st, &write_opts, 0), 0);\n \t\tcl_assert_equal_i(reftable_addition_add(add,\n \t\t\t\t\t\t\twrite_test_ref, &ref), 0);\n \t\tcl_assert_equal_i(reftable_addition_commit(add), 0);\n@@ -398,15 +393,14 @@ void test_reftable_stack__auto_compaction_fails_gracefully(void)\n \t\t.value_type = REFTABLE_REF_VAL1,\n \t\t.value.val1 = {0x01},\n \t};\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st;\n \tstruct reftable_buf table_path = REFTABLE_BUF_INIT;\n \tchar *dir = get_tmp_dir(__LINE__);\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t     &ref, 0), 0);\n+\t\t\t\t\t     &ref, NULL, 0), 0);\n \tcl_assert_equal_i(st->merged->tables_len, 1);\n \tcl_assert_equal_i(st->stats.attempts, 0);\n \tcl_assert_equal_i(st->stats.failures, 0);\n@@ -424,7 +418,7 @@ void test_reftable_stack__auto_compaction_fails_gracefully(void)\n \twrite_file_buf(table_path.buf, \"\", 0);\n \n \tref.update_index = 2;\n-\terr = reftable_stack_add(st, write_test_ref, &ref, 0);\n+\terr = reftable_stack_add(st, write_test_ref, &ref, NULL, 0);\n \tcl_assert(!err);\n \tcl_assert_equal_i(st->merged->tables_len, 2);\n \tcl_assert_equal_i(st->stats.attempts, 1);\n@@ -443,7 +437,6 @@ static int write_error(struct reftable_writer *wr UNUSED, void *arg)\n void test_reftable_stack__update_index_check(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tstruct reftable_ref_record ref1 = {\n \t\t.refname = (char *) \"name1\",\n@@ -458,11 +451,11 @@ void test_reftable_stack__update_index_check(void)\n \t\t.value.symref = (char *) \"master\",\n \t};\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t     &ref1, 0), 0);\n+\t\t\t\t\t     &ref1, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t     &ref2, 0), REFTABLE_API_ERROR);\n+\t\t\t\t\t     &ref2, NULL, 0), REFTABLE_API_ERROR);\n \treftable_stack_destroy(st);\n \tclear_dir(dir);\n }\n@@ -470,14 +463,13 @@ void test_reftable_stack__update_index_check(void)\n void test_reftable_stack__lock_failure(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tint i;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \tfor (i = -1; i != REFTABLE_EMPTY_TABLE_ERROR; i--)\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_error,\n-\t\t\t\t\t\t     &i, 0), i);\n+\t\t\t\t\t\t     &i, NULL, 0), i);\n \n \treftable_stack_destroy(st);\n \tclear_dir(dir);\n@@ -499,7 +491,7 @@ void test_reftable_stack__add(void)\n \tsize_t i, N = ARRAY_SIZE(refs);\n \tint err = 0;\n \n-\terr = reftable_new_stack(&st, dir, &opts);\n+\terr = reftable_new_stack(&st, dir, NULL);\n \tcl_assert(!err);\n \n \tfor (i = 0; i < N; i++) {\n@@ -521,7 +513,7 @@ void test_reftable_stack__add(void)\n \n \tfor (i = 0; i < N; i++)\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t\t     &refs[i], 0), 0);\n+\t\t\t\t\t\t     &refs[i], &opts, 0), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\tstruct write_log_arg arg = {\n@@ -529,10 +521,10 @@ void test_reftable_stack__add(void)\n \t\t\t.update_index = reftable_stack_next_update_index(st),\n \t\t};\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t\t     &arg, &opts, 0), 0);\n \t}\n \n-\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st, &opts, NULL), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\tstruct reftable_ref_record dest = { 0 };\n@@ -584,7 +576,6 @@ void test_reftable_stack__add(void)\n \n void test_reftable_stack__iterator(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \tstruct reftable_ref_record refs[10] = { 0 };\n@@ -593,7 +584,7 @@ void test_reftable_stack__iterator(void)\n \tsize_t N = ARRAY_SIZE(refs), i;\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\trefs[i].refname = xstrfmt(\"branch%02\"PRIuMAX, (uintmax_t)i);\n@@ -613,7 +604,7 @@ void test_reftable_stack__iterator(void)\n \n \tfor (i = 0; i < N; i++)\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t\t     &refs[i], 0), 0);\n+\t\t\t\t\t\t     &refs[i], NULL, 0), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\tstruct write_log_arg arg = {\n@@ -622,7 +613,7 @@ void test_reftable_stack__iterator(void)\n \t\t};\n \n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t\t     &arg, NULL, 0), 0);\n \t}\n \n \treftable_stack_init_ref_iterator(st, &it);\n@@ -669,9 +660,6 @@ void test_reftable_stack__iterator(void)\n \n void test_reftable_stack__log_normalize(void)\n {\n-\tstruct reftable_write_options opts = {\n-\t\t0,\n-\t};\n \tstruct reftable_stack *st = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \tstruct reftable_log_record input = {\n@@ -693,15 +681,15 @@ void test_reftable_stack__log_normalize(void)\n \t\t.update_index = 1,\n \t};\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tinput.value.update.message = (char *) \"one\\ntwo\";\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t     &arg, 0), REFTABLE_API_ERROR);\n+\t\t\t\t\t     &arg, NULL, 0), REFTABLE_API_ERROR);\n \n \tinput.value.update.message = (char *) \"one\";\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t     &arg, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_stack_read_log(st, input.refname,\n \t\t\t\t\t\t  &dest), 0);\n \tcl_assert_equal_s(dest.value.update.message, \"one\\n\");\n@@ -709,7 +697,7 @@ void test_reftable_stack__log_normalize(void)\n \tinput.value.update.message = (char *) \"two\\n\";\n \targ.update_index = 2;\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t     &arg, NULL, 0), 0);\n \tcl_assert_equal_i(reftable_stack_read_log(st, input.refname,\n \t\t\t\t\t\t  &dest), 0);\n \tcl_assert_equal_s(dest.value.update.message, \"two\\n\");\n@@ -723,7 +711,6 @@ void test_reftable_stack__log_normalize(void)\n void test_reftable_stack__tombstone(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tstruct reftable_ref_record refs[2] = { 0 };\n \tstruct reftable_log_record logs[2] = { 0 };\n@@ -731,7 +718,7 @@ void test_reftable_stack__tombstone(void)\n \tstruct reftable_ref_record dest = { 0 };\n \tstruct reftable_log_record log_dest = { 0 };\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \t/* even entries add the refs, odd entries delete them. */\n \tfor (i = 0; i < N; i++) {\n@@ -760,7 +747,7 @@ void test_reftable_stack__tombstone(void)\n \t}\n \tfor (i = 0; i < N; i++)\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t\t     &refs[i], 0), 0);\n+\t\t\t\t\t\t     &refs[i], NULL, 0), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\tstruct write_log_arg arg = {\n@@ -768,7 +755,7 @@ void test_reftable_stack__tombstone(void)\n \t\t\t.update_index = reftable_stack_next_update_index(st),\n \t\t};\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t\t     &arg, NULL, 0), 0);\n \t}\n \n \tcl_assert_equal_i(reftable_stack_read_ref(st, \"branch\",\n@@ -779,7 +766,7 @@ void test_reftable_stack__tombstone(void)\n \t\t\t\t\t\t  &log_dest), 1);\n \treftable_log_record_release(&log_dest);\n \n-\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_read_ref(st, \"branch\",\n \t\t\t\t\t\t  &dest), 1);\n \tcl_assert_equal_i(reftable_stack_read_log(st, \"branch\",\n@@ -799,7 +786,6 @@ void test_reftable_stack__tombstone(void)\n void test_reftable_stack__hash_id(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \n \tstruct reftable_ref_record ref = {\n@@ -808,15 +794,14 @@ void test_reftable_stack__hash_id(void)\n \t\t.value.symref = (char *) \"target\",\n \t\t.update_index = 1,\n \t};\n-\tstruct reftable_write_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 };\n+\tstruct reftable_stack_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 };\n \tstruct reftable_stack *st32 = NULL;\n-\tstruct reftable_write_options opts_default = { 0 };\n \tstruct reftable_stack *st_default = NULL;\n \tstruct reftable_ref_record dest = { 0 };\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t     &ref, 0), 0);\n+\t\t\t\t\t     &ref, NULL, 0), 0);\n \n \t/* can't read it with the wrong hash ID. */\n \tcl_assert_equal_i(reftable_new_stack(&st32, dir,\n@@ -824,7 +809,7 @@ void test_reftable_stack__hash_id(void)\n \n \t/* check that we can read it back with default opts too. */\n \tcl_assert_equal_i(reftable_new_stack(&st_default, dir,\n-\t\t\t\t\t     &opts_default), 0);\n+\t\t\t\t\t     NULL), 0);\n \tcl_assert_equal_i(reftable_stack_read_ref(st_default, \"master\",\n \t\t\t\t\t\t  &dest), 0);\n \tcl_assert(reftable_ref_record_equal(&ref, &dest,\n@@ -855,7 +840,6 @@ void test_reftable_stack__suggest_compaction_segment_nothing(void)\n void test_reftable_stack__reflog_expire(void)\n {\n \tchar *dir = get_tmp_dir(__LINE__);\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tstruct reftable_log_record logs[20] = { 0 };\n \tsize_t i, N = ARRAY_SIZE(logs) - 1;\n@@ -864,7 +848,7 @@ void test_reftable_stack__reflog_expire(void)\n \t};\n \tstruct reftable_log_record log = { 0 };\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (i = 1; i <= N; i++) {\n \t\tchar buf[256];\n@@ -885,18 +869,18 @@ void test_reftable_stack__reflog_expire(void)\n \t\t\t.update_index = reftable_stack_next_update_index(st),\n \t\t};\n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_log,\n-\t\t\t\t\t\t     &arg, 0), 0);\n+\t\t\t\t\t\t     &arg, NULL, 0), 0);\n \t}\n \n-\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL), 0);\n-\tcl_assert_equal_i(reftable_stack_compact_all(st, &expiry), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL, NULL), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL, &expiry), 0);\n \tcl_assert_equal_i(reftable_stack_read_log(st, logs[9].refname,\n \t\t\t\t\t\t  &log), 1);\n \tcl_assert_equal_i(reftable_stack_read_log(st, logs[11].refname,\n \t\t\t\t\t\t  &log), 0);\n \n \texpiry.min_update_index = 15;\n-\tcl_assert_equal_i(reftable_stack_compact_all(st, &expiry), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st, NULL, &expiry), 0);\n \tcl_assert_equal_i(reftable_stack_read_log(st, logs[14].refname,\n \t\t\t\t\t\t  &log), 1);\n \tcl_assert_equal_i(reftable_stack_read_log(st, logs[16].refname,\n@@ -918,15 +902,14 @@ static int write_nothing(struct reftable_writer *wr, void *arg UNUSED)\n \n void test_reftable_stack__empty_add(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \tstruct reftable_stack *st2 = NULL;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_add(st, write_nothing,\n-\t\t\t\t\t     NULL, 0), 0);\n-\tcl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0);\n+\t\t\t\t\t     NULL, NULL, 0), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0);\n \tclear_dir(dir);\n \treftable_stack_destroy(st);\n \treftable_stack_destroy(st2);\n@@ -952,7 +935,7 @@ void test_reftable_stack__auto_compaction(void)\n \tsize_t i, N = 100;\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (i = 0; i < N; i++) {\n \t\tchar name[100];\n@@ -964,10 +947,10 @@ void test_reftable_stack__auto_compaction(void)\n \t\t};\n \t\tsnprintf(name, sizeof(name), \"branch%04\"PRIuMAX, (uintmax_t)i);\n \n-\t\terr = reftable_stack_add(st, write_test_ref, &ref, 0);\n+\t\terr = reftable_stack_add(st, write_test_ref, &ref, &opts, 0);\n \t\tcl_assert(!err);\n \n-\t\terr = reftable_stack_auto_compact(st);\n+\t\terr = reftable_stack_auto_compact(st, &opts);\n \t\tcl_assert(!err);\n \t\tcl_assert(i < 2 || st->merged->tables_len < 2 * fastlogN(i, 2));\n \t}\n@@ -989,7 +972,7 @@ void test_reftable_stack__auto_compaction_factor(void)\n \tsize_t N = 100;\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (size_t i = 0; i < N; i++) {\n \t\tchar name[20];\n@@ -1000,7 +983,7 @@ void test_reftable_stack__auto_compaction_factor(void)\n \t\t};\n \t\txsnprintf(name, sizeof(name), \"branch%04\"PRIuMAX, (uintmax_t)i);\n \n-\t\terr = reftable_stack_add(st, &write_test_ref, &ref, 0);\n+\t\terr = reftable_stack_add(st, &write_test_ref, &ref, &opts, 0);\n \t\tcl_assert(!err);\n \n \t\tcl_assert(i < 5 || st->merged->tables_len < 5 * fastlogN(i, 5));\n@@ -1020,7 +1003,7 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void)\n \tchar *dir = get_tmp_dir(__LINE__);\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \twrite_n_ref_tables(st, 5);\n \tcl_assert_equal_i(st->merged->tables_len, 5);\n@@ -1042,7 +1025,7 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void)\n \t * would in theory compact all tables, due to the preexisting lock we\n \t * only compact the newest two tables.\n \t */\n-\terr = reftable_stack_auto_compact(st);\n+\terr = reftable_stack_auto_compact(st, &opts);\n \tcl_assert(!err);\n \tcl_assert_equal_i(st->stats.failures, 0);\n \tcl_assert_equal_i(st->merged->tables_len, 4);\n@@ -1054,12 +1037,11 @@ void test_reftable_stack__auto_compaction_with_locked_tables(void)\n \n void test_reftable_stack__add_performs_auto_compaction(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \tsize_t i, n = 20;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \tfor (i = 0; i <= n; i++) {\n \t\tstruct reftable_ref_record ref = {\n@@ -1067,6 +1049,9 @@ void test_reftable_stack__add_performs_auto_compaction(void)\n \t\t\t.value_type = REFTABLE_REF_SYMREF,\n \t\t\t.value.symref = (char *) \"master\",\n \t\t};\n+\t\tstruct reftable_write_options write_opts = {\n+\t\t\t.disable_auto_compact = (i != n),\n+\t\t};\n \t\tbool required = false;\n \t\tchar buf[128];\n \n@@ -1075,20 +1060,18 @@ void test_reftable_stack__add_performs_auto_compaction(void)\n \t\t * we can ensure that we indeed honor this setting and have\n \t\t * better control over when exactly auto compaction runs.\n \t\t */\n-\t\tst->opts.disable_auto_compact = i != n;\n-\n \t\tsnprintf(buf, sizeof(buf), \"branch-%04\"PRIuMAX, (uintmax_t)i);\n \t\tref.refname = buf;\n \n \t\tcl_assert_equal_i(reftable_stack_add(st, write_test_ref,\n-\t\t\t\t\t\t     &ref, 0), 0);\n+\t\t\t\t\t\t     &ref, &write_opts, 0), 0);\n \n \t\t/*\n \t\t * The stack length should grow continuously for all runs where\n \t\t * auto compaction is disabled. When enabled, we should merge\n \t\t * all tables in the stack.\n \t\t */\n-\t\tcl_assert_equal_i(reftable_stack_compaction_required(st, true, &required), 0);\n+\t\tcl_assert_equal_i(reftable_stack_compaction_required(st, NULL, true, &required), 0);\n \t\tif (i != n) {\n \t\t\tcl_assert_equal_i(st->merged->tables_len, i + 1);\n \t\t\tif (i < 1)\n@@ -1115,7 +1098,7 @@ void test_reftable_stack__compaction_with_locked_tables(void)\n \tchar *dir = get_tmp_dir(__LINE__);\n \tint err;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \twrite_n_ref_tables(st, 3);\n \tcl_assert_equal_i(st->merged->tables_len, 3);\n@@ -1131,7 +1114,7 @@ void test_reftable_stack__compaction_with_locked_tables(void)\n \t * Compaction is expected to fail given that we were not able to\n \t * compact all tables.\n \t */\n-\terr = reftable_stack_compact_all(st, NULL);\n+\terr = reftable_stack_compact_all(st, &opts, NULL);\n \tcl_assert_equal_i(err, REFTABLE_LOCK_ERROR);\n \tcl_assert_equal_i(st->stats.failures, 1);\n \tcl_assert_equal_i(st->merged->tables_len, 3);\n@@ -1143,15 +1126,14 @@ void test_reftable_stack__compaction_with_locked_tables(void)\n \n void test_reftable_stack__compaction_concurrent(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st1 = NULL, *st2 = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \n-\tcl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0);\n \twrite_n_ref_tables(st1, 3);\n \n-\tcl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0);\n-\tcl_assert_equal_i(reftable_stack_compact_all(st1, NULL), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st1, NULL, NULL), 0);\n \n \treftable_stack_destroy(st1);\n \treftable_stack_destroy(st2);\n@@ -1171,20 +1153,19 @@ static void unclean_stack_close(struct reftable_stack *st)\n \n void test_reftable_stack__compaction_concurrent_clean(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st1 = NULL, *st2 = NULL, *st3 = NULL;\n \tchar *dir = get_tmp_dir(__LINE__);\n \n-\tcl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0);\n \twrite_n_ref_tables(st1, 3);\n \n-\tcl_assert_equal_i(reftable_new_stack(&st2, dir, &opts), 0);\n-\tcl_assert_equal_i(reftable_stack_compact_all(st1, NULL), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st2, dir, NULL), 0);\n+\tcl_assert_equal_i(reftable_stack_compact_all(st1, NULL, NULL), 0);\n \n \tunclean_stack_close(st1);\n \tunclean_stack_close(st2);\n \n-\tcl_assert_equal_i(reftable_new_stack(&st3, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st3, dir, NULL), 0);\n \tcl_assert_equal_i(reftable_stack_clean(st3), 0);\n \tcl_assert_equal_i(count_dir_entries(dir), 2);\n \n@@ -1197,7 +1178,6 @@ void test_reftable_stack__compaction_concurrent_clean(void)\n \n void test_reftable_stack__read_across_reload(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st1 = NULL, *st2 = NULL;\n \tstruct reftable_ref_record rec = { 0 };\n \tstruct reftable_iterator it = { 0 };\n@@ -1205,17 +1185,17 @@ void test_reftable_stack__read_across_reload(void)\n \tint err;\n \n \t/* Create a first stack and set up an iterator for it. */\n-\tcl_assert_equal_i(reftable_new_stack(&st1, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st1, dir, NULL), 0);\n \twrite_n_ref_tables(st1, 2);\n \tcl_assert_equal_i(st1->merged->tables_len, 2);\n \treftable_stack_init_ref_iterator(st1, &it);\n \tcl_assert_equal_i(reftable_iterator_seek_ref(&it, \"\"), 0);\n \n \t/* Set up a second stack for the same directory and compact it. */\n-\terr = reftable_new_stack(&st2, dir, &opts);\n+\terr = reftable_new_stack(&st2, dir, NULL);\n \tcl_assert(!err);\n \tcl_assert_equal_i(st2->merged->tables_len, 2);\n-\terr = reftable_stack_compact_all(st2, NULL);\n+\terr = reftable_stack_compact_all(st2, NULL, NULL);\n \tcl_assert(!err);\n \tcl_assert_equal_i(st2->merged->tables_len, 1);\n \n@@ -1244,7 +1224,6 @@ void test_reftable_stack__read_across_reload(void)\n \n void test_reftable_stack__reload_with_missing_table(void)\n {\n-\tstruct reftable_write_options opts = { 0 };\n \tstruct reftable_stack *st = NULL;\n \tstruct reftable_ref_record rec = { 0 };\n \tstruct reftable_iterator it = { 0 };\n@@ -1253,7 +1232,7 @@ void test_reftable_stack__reload_with_missing_table(void)\n \tint err;\n \n \t/* Create a first stack and set up an iterator for it. */\n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \twrite_n_ref_tables(st, 2);\n \tcl_assert_equal_i(st->merged->tables_len, 2);\n \treftable_stack_init_ref_iterator(st, &it);\n@@ -1320,11 +1299,11 @@ void test_reftable_stack__invalid_limit_updates(void)\n \tchar *dir = get_tmp_dir(__LINE__);\n \tstruct reftable_stack *st = NULL;\n \n-\tcl_assert_equal_i(reftable_new_stack(&st, dir, &opts), 0);\n+\tcl_assert_equal_i(reftable_new_stack(&st, dir, NULL), 0);\n \n \treftable_addition_destroy(add);\n \n-\tcl_assert_equal_i(reftable_stack_new_addition(&add, st, 0), 0);\n+\tcl_assert_equal_i(reftable_stack_new_addition(&add, st, &opts, 0), 0);\n \n \t/*\n \t * write_limits_after_ref also updates the update indexes after adding\ndiff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c\nindex 14fae8b199..fae478ee04 100644\n--- a/t/unit-tests/u-reftable-table.c\n+++ b/t/unit-tests/u-reftable-table.c\n@@ -22,7 +22,8 @@ void test_reftable_table__seek_once(void)\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n \tint ret;\n \n-\tcl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL);\n+\tcl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0,\n+\t\t\t\t REFTABLE_HASH_SHA1, NULL);\n \tblock_source_from_buf(&source, &buf);\n \n \tret = reftable_table_new(&table, &source, \"name\");\n@@ -64,7 +65,7 @@ void test_reftable_table__reseek(void)\n \tint ret;\n \n \tcl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records),\n-\t\t\t\t NULL, 0, NULL);\n+\t\t\t\t NULL, 0, REFTABLE_HASH_SHA1, NULL);\n \tblock_source_from_buf(&source, &buf);\n \n \tret = reftable_table_new(&table, &source, \"name\");\n@@ -147,7 +148,8 @@ void test_reftable_table__block_iterator(void)\n \t\t\t\t\t     (uintmax_t) i);\n \t}\n \n-\tcl_reftable_write_to_buf(&buf, records, nrecords, NULL, 0, NULL);\n+\tcl_reftable_write_to_buf(&buf, records, nrecords, NULL, 0,\n+\t\t\t\t REFTABLE_HASH_SHA1, NULL);\n \tblock_source_from_buf(&source, &buf);\n \n \tret = reftable_table_new(&table, &source, \"name\");\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546132","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-10-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 10/11] refs/reftable: lazy-load configuration to fix chicken-and-egg","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:32Z","receivedAt":"2026-06-22T08:28:56Z","isPatch":true,"body":"Same as with the \"files\" backend, the \"reftable\" backend also has a\nchicken-and-egg problem with \"onbranch\" conditions. Fix this issue the\nsame as we did with the \"files\" backend by lazy-loading configuration.\n\nNow that both the \"files\" and the \"reftable\" backend handle this\nproperly, add a generic test to t1400 that verifies that the user can\nconfigure \"core.logAllRefUpdates\" via an \"onbranch\" condition. This is\nmostly a nonsensical thing to do in the first place, but it serves as a\ngood sanity chekc.\n\nNote that we had to move `should_write_log()` around so that it can\naccess the new `reftable_be_write_options()` function.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs/reftable-backend.c           | 146 ++++++++++++++++++++++----------------\n t/t0613-reftable-write-options.sh |  19 +++++\n t/t1400-update-ref.sh             |  12 ++++\n 3 files changed, 116 insertions(+), 61 deletions(-)\n\ndiff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\nindex 608d71cf10..d74131a5ae 100644\n--- a/refs/reftable-backend.c\n+++ b/refs/reftable-backend.c\n@@ -141,10 +141,21 @@ struct reftable_ref_store {\n \t */\n \tstruct strmap worktree_backends;\n \tstruct reftable_stack_options stack_options;\n-\tstruct reftable_write_options write_options;\n+\n+\t/*\n+\t * Options used when writing to or compacting the reftable stacks.\n+\t * These are parsed from the configuration lazily on first use via\n+\t * `reftable_be_write_options()` so that we don't have to access the\n+\t * configuration when initializing the ref store. Do not access these\n+\t * fields directly, but use the accessor instead.\n+\t */\n+\tstruct reftable_be_write_options {\n+\t\tstruct reftable_write_options opts;\n+\t\tenum log_refs_config log_all_ref_updates;\n+\t\tbool initialized;\n+\t} write_opts_lazy_loaded;\n \n \tunsigned int store_flags;\n-\tenum log_refs_config log_all_ref_updates;\n \tint err;\n };\n \n@@ -285,26 +296,6 @@ static int backend_for(struct reftable_backend **out,\n \treturn ret;\n }\n \n-static int should_write_log(struct reftable_ref_store *refs, const char *refname)\n-{\n-\tenum log_refs_config log_refs_cfg = refs->log_all_ref_updates;\n-\tif (log_refs_cfg == LOG_REFS_UNSET)\n-\t\tlog_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL;\n-\n-\tswitch (log_refs_cfg) {\n-\tcase LOG_REFS_NONE:\n-\t\treturn refs_reflog_exists(&refs->base, refname);\n-\tcase LOG_REFS_ALWAYS:\n-\t\treturn 1;\n-\tcase LOG_REFS_NORMAL:\n-\t\tif (should_autocreate_reflog(log_refs_cfg, refname))\n-\t\t\treturn 1;\n-\t\treturn refs_reflog_exists(&refs->base, refname);\n-\tdefault:\n-\t\tBUG(\"unhandled core.logAllRefUpdates value %d\", log_refs_cfg);\n-\t}\n-}\n-\n static void fill_reftable_log_record(struct reftable_log_record *log, const struct ident_split *split)\n {\n \tconst char *tz_begin;\n@@ -336,38 +327,72 @@ static int reftable_be_config(const char *var, const char *value,\n \t\t\t      void *payload)\n {\n \tstruct reftable_ref_store *refs = payload;\n+\tstruct reftable_be_write_options *opts = &refs->write_opts_lazy_loaded;\n \n \tif (!strcmp(var, \"reftable.blocksize\")) {\n \t\tunsigned long block_size = git_config_ulong(var, value, ctx->kvi);\n \t\tif (block_size > 16777215)\n \t\t\tdie(\"reftable block size cannot exceed 16MB\");\n-\t\trefs->write_options.block_size = block_size;\n+\t\topts->opts.block_size = block_size;\n \t} else if (!strcmp(var, \"reftable.restartinterval\")) {\n \t\tunsigned long restart_interval = git_config_ulong(var, value, ctx->kvi);\n \t\tif (restart_interval > UINT16_MAX)\n \t\t\tdie(\"reftable block size cannot exceed %u\", (unsigned)UINT16_MAX);\n-\t\trefs->write_options.restart_interval = restart_interval;\n+\t\topts->opts.restart_interval = restart_interval;\n \t} else if (!strcmp(var, \"reftable.indexobjects\")) {\n-\t\trefs->write_options.skip_index_objects = !git_config_bool(var, value);\n+\t\topts->opts.skip_index_objects = !git_config_bool(var, value);\n \t} else if (!strcmp(var, \"reftable.geometricfactor\")) {\n \t\tunsigned long factor = git_config_ulong(var, value, ctx->kvi);\n \t\tif (factor > UINT8_MAX)\n \t\t\tdie(\"reftable geometric factor cannot exceed %u\", (unsigned)UINT8_MAX);\n-\t\trefs->write_options.auto_compaction_factor = factor;\n+\t\topts->opts.auto_compaction_factor = factor;\n \t} else if (!strcmp(var, \"reftable.locktimeout\")) {\n \t\tint64_t lock_timeout = git_config_int64(var, value, ctx->kvi);\n \t\tif (lock_timeout > LONG_MAX)\n \t\t\tdie(\"reftable lock timeout cannot exceed %\"PRIdMAX, (intmax_t)LONG_MAX);\n \t\tif (lock_timeout < 0 && lock_timeout != -1)\n \t\t\tdie(\"reftable lock timeout does not support negative values other than -1\");\n-\t\trefs->write_options.lock_timeout_ms = lock_timeout;\n+\t\topts->opts.lock_timeout_ms = lock_timeout;\n \t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n-\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n+\t\topts->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n \t}\n \n \treturn 0;\n }\n \n+static const struct reftable_be_write_options *reftable_be_write_options(struct reftable_ref_store *refs)\n+{\n+\tstruct reftable_be_write_options *opts = &refs->write_opts_lazy_loaded;\n+\tmode_t mask;\n+\n+\tif (opts->initialized)\n+\t\treturn opts;\n+\n+\tmask = umask(0);\n+\tumask(mask);\n+\n+\topts->opts.default_permissions = calc_shared_perm(refs->base.repo, 0666 & ~mask);\n+\topts->opts.disable_auto_compact =\n+\t\t!git_env_bool(\"GIT_TEST_REFTABLE_AUTOCOMPACTION\", 1);\n+\topts->opts.lock_timeout_ms = 100;\n+\topts->log_all_ref_updates = LOG_REFS_UNSET;\n+\n+\trepo_config(refs->base.repo, reftable_be_config, refs);\n+\n+\t/*\n+\t * It is somewhat unfortunate that we have to mirror the default block\n+\t * size of the reftable library here. But given that the write options\n+\t * wouldn't be updated by the library here, and given that we require\n+\t * the proper block size to trim reflog message so that they fit, we\n+\t * must set up a proper value here.\n+\t */\n+\tif (!opts->opts.block_size)\n+\t\topts->opts.block_size = 4096;\n+\n+\topts->initialized = true;\n+\treturn opts;\n+}\n+\n static void reftable_be_reparent(const char *name UNUSED,\n \t\t\t\t const char *old_cwd,\n \t\t\t\t const char *new_cwd,\n@@ -391,10 +416,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \tstruct strbuf refdir = STRBUF_INIT;\n \tstruct strbuf path = STRBUF_INIT;\n \tbool is_worktree;\n-\tmode_t mask;\n-\n-\tmask = umask(0);\n-\tumask(mask);\n \n \trefs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir,\n \t\t\t\t\t &ref_common_dir);\n@@ -413,23 +434,6 @@ static struct ref_store *reftable_be_init(struct repository *repo,\n \tdefault:\n \t\tBUG(\"unknown hash algorithm %d\", repo->hash_algo->format_id);\n \t}\n-\trefs->write_options.default_permissions = calc_shared_perm(repo, 0666 & ~mask);\n-\trefs->write_options.disable_auto_compact =\n-\t\t!git_env_bool(\"GIT_TEST_REFTABLE_AUTOCOMPACTION\", 1);\n-\trefs->write_options.lock_timeout_ms = 100;\n-\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n-\n-\trepo_config(repo, reftable_be_config, refs);\n-\n-\t/*\n-\t * It is somewhat unfortunate that we have to mirror the default block\n-\t * size of the reftable library here. But given that the write options\n-\t * wouldn't be updated by the library here, and given that we require\n-\t * the proper block size to trim reflog message so that they fit, we\n-\t * must set up a proper value here.\n-\t */\n-\tif (!refs->write_options.block_size)\n-\t\trefs->write_options.block_size = 4096;\n \n \t/*\n \t * Set up the main reftable stack that is hosted in GIT_COMMON_DIR.\n@@ -998,7 +1002,7 @@ static int prepare_transaction_update(struct write_transaction_table_arg **out,\n \t\tstruct reftable_addition *addition;\n \n \t\tret = reftable_stack_new_addition(&addition, be->stack,\n-\t\t\t\t\t\t  &refs->write_options,\n+\t\t\t\t\t\t  &reftable_be_write_options(refs)->opts,\n \t\t\t\t\t\t  REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \t\tif (ret) {\n \t\t\tif (ret == REFTABLE_LOCK_ERROR)\n@@ -1437,6 +1441,26 @@ static int transaction_update_cmp(const void *a, const void *b)\n \treturn strcmp(update_a->update->refname, update_b->update->refname);\n }\n \n+static int should_write_log(struct reftable_ref_store *refs, const char *refname)\n+{\n+\tenum log_refs_config log_refs_cfg = reftable_be_write_options(refs)->log_all_ref_updates;\n+\tif (log_refs_cfg == LOG_REFS_UNSET)\n+\t\tlog_refs_cfg = is_bare_repository() ? LOG_REFS_NONE : LOG_REFS_NORMAL;\n+\n+\tswitch (log_refs_cfg) {\n+\tcase LOG_REFS_NONE:\n+\t\treturn refs_reflog_exists(&refs->base, refname);\n+\tcase LOG_REFS_ALWAYS:\n+\t\treturn 1;\n+\tcase LOG_REFS_NORMAL:\n+\t\tif (should_autocreate_reflog(log_refs_cfg, refname))\n+\t\t\treturn 1;\n+\t\treturn refs_reflog_exists(&refs->base, refname);\n+\tdefault:\n+\t\tBUG(\"unhandled core.logAllRefUpdates value %d\", log_refs_cfg);\n+\t}\n+}\n+\n static int write_transaction_table(struct reftable_writer *writer, void *cb_data)\n {\n \tstruct write_transaction_table_arg *arg = cb_data;\n@@ -1571,7 +1595,7 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data\n \t\t\t\tmemcpy(log->value.update.old_hash,\n \t\t\t\t       tx_update->current_oid.hash, GIT_MAX_RAWSZ);\n \t\t\t\tlog->value.update.message =\n-\t\t\t\t\txstrndup(u->msg, arg->refs->write_options.block_size / 2);\n+\t\t\t\t\txstrndup(u->msg, reftable_be_write_options(arg->refs)->opts.block_size / 2);\n \t\t\t}\n \t\t}\n \n@@ -1687,9 +1711,9 @@ static int reftable_be_optimize(struct ref_store *ref_store,\n \t\tstack = refs->main_backend.stack;\n \n \tif (opts->flags & REFS_OPTIMIZE_AUTO)\n-\t\tret = reftable_stack_auto_compact(stack, &refs->write_options);\n+\t\tret = reftable_stack_auto_compact(stack, &reftable_be_write_options(refs)->opts);\n \telse\n-\t\tret = reftable_stack_compact_all(stack, &refs->write_options, NULL);\n+\t\tret = reftable_stack_compact_all(stack, &reftable_be_write_options(refs)->opts, NULL);\n \tif (ret < 0) {\n \t\tret = error(_(\"unable to compact stack: %s\"),\n \t\t\t    reftable_error_str(ret));\n@@ -1723,7 +1747,7 @@ static int reftable_be_optimize_required(struct ref_store *ref_store,\n \tif (opts->flags & REFS_OPTIMIZE_AUTO)\n \t\tuse_heuristics = true;\n \n-\treturn reftable_stack_compaction_required(stack, &refs->write_options,\n+\treturn reftable_stack_compaction_required(stack, &reftable_be_write_options(refs)->opts,\n \t\t\t\t\t\t  use_heuristics, required);\n }\n \n@@ -1843,7 +1867,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data)\n \t\tlogs[logs_nr].refname = xstrdup(arg->newname);\n \t\tlogs[logs_nr].update_index = deletion_ts;\n \t\tlogs[logs_nr].value.update.message =\n-\t\t\txstrndup(arg->logmsg, arg->refs->write_options.block_size / 2);\n+\t\t\txstrndup(arg->logmsg, reftable_be_write_options(arg->refs)->opts.block_size / 2);\n \t\tmemcpy(logs[logs_nr].value.update.old_hash, old_ref.value.val1, GIT_MAX_RAWSZ);\n \t\tlogs_nr++;\n \n@@ -1882,7 +1906,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data)\n \tlogs[logs_nr].refname = xstrdup(arg->newname);\n \tlogs[logs_nr].update_index = creation_ts;\n \tlogs[logs_nr].value.update.message =\n-\t\txstrndup(arg->logmsg, arg->refs->write_options.block_size / 2);\n+\t\txstrndup(arg->logmsg, reftable_be_write_options(arg->refs)->opts.block_size / 2);\n \tmemcpy(logs[logs_nr].value.update.new_hash, old_ref.value.val1, GIT_MAX_RAWSZ);\n \tlogs_nr++;\n \n@@ -1981,7 +2005,7 @@ static int reftable_be_rename_ref(struct ref_store *ref_store,\n \tif (ret)\n \t\tgoto done;\n \tret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg,\n-\t\t\t\t &refs->write_options,\n+\t\t\t\t &reftable_be_write_options(refs)->opts,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2012,7 +2036,7 @@ static int reftable_be_copy_ref(struct ref_store *ref_store,\n \tif (ret)\n \t\tgoto done;\n \tret = reftable_stack_add(arg.be->stack, &write_copy_table, &arg,\n-\t\t\t\t &refs->write_options,\n+\t\t\t\t &reftable_be_write_options(refs)->opts,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2378,7 +2402,7 @@ static int reftable_be_create_reflog(struct ref_store *ref_store,\n \targ.stack = be->stack;\n \n \tret = reftable_stack_add(be->stack, &write_reflog_existence_table, &arg,\n-\t\t\t\t &refs->write_options,\n+\t\t\t\t &reftable_be_write_options(refs)->opts,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n done:\n@@ -2451,7 +2475,7 @@ static int reftable_be_delete_reflog(struct ref_store *ref_store,\n \targ.stack = be->stack;\n \n \tret = reftable_stack_add(be->stack, &write_reflog_delete_table, &arg,\n-\t\t\t\t &refs->write_options,\n+\t\t\t\t &reftable_be_write_options(refs)->opts,\n \t\t\t\t REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \n \tassert(ret != REFTABLE_API_ERROR);\n@@ -2574,7 +2598,7 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store,\n \t\tgoto done;\n \n \tret = reftable_stack_new_addition(&add, be->stack,\n-\t\t\t\t\t  &refs->write_options,\n+\t\t\t\t\t  &reftable_be_write_options(refs)->opts,\n \t\t\t\t\t  REFTABLE_STACK_NEW_ADDITION_RELOAD);\n \tif (ret < 0)\n \t\tgoto done;\ndiff --git a/t/t0613-reftable-write-options.sh b/t/t0613-reftable-write-options.sh\nindex 26b716c75f..a65960d048 100755\n--- a/t/t0613-reftable-write-options.sh\n+++ b/t/t0613-reftable-write-options.sh\n@@ -278,4 +278,23 @@ test_expect_success 'object index can be disabled' '\n \t)\n '\n \n+test_expect_success 'write options can be set up via onbranch condition' '\n+\ttest_config_global core.logAllRefUpdates false &&\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tinit_repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit A &&\n+\t\ttest_commit B &&\n+\t\tcat >.git/include <<-\\EOF &&\n+\t\t[reftable]\n+\t\t\tblockSize = 123\n+\t\tEOF\n+\t\tgit config includeIf.onbranch:master.path \"$(pwd)/.git/include\" &&\n+\t\tgit refs optimize &&\n+\t\ttest-tool dump-reftable -b .git/reftable/*.ref >stats &&\n+\t\ttest_grep \"block_size: 123\" stats\n+\t)\n+'\n+\n test_done\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 1015f335e3..b8c3be6631 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -178,6 +178,18 @@ test_expect_success '--no-create-reflog overrides core.logAllRefUpdates=always'\n \ttest_must_fail git reflog exists $outside\n '\n \n+test_expect_success 'core.logAllRefUpdates can be set up via onbranch condition' '\n+\ttest_when_finished \"git update-ref -d $outside\" &&\n+\ttest_when_finished \"rm -f .git/include\" &&\n+\tcat >.git/include <<-\\EOF &&\n+\t[core]\n+\t\tlogAllRefUpdates = always\n+\tEOF\n+\ttest_config includeIf.onbranch:main.path \"$(pwd)/.git/include\" &&\n+\tgit update-ref $outside $A &&\n+\tgit reflog exists $outside\n+'\n+\n test_expect_success \"create $m (by HEAD)\" '\n \tgit update-ref HEAD $A &&\n \ttest $A = $(git show-ref -s --verify $m)\n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546134","messageId":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-11-018475013dbc@pks.im","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-0-018475013dbc@pks.im","subject":"[PATCH v5 11/11] refs: protect against chicken-and-egg recursion","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-22T08:28:33Z","receivedAt":"2026-06-22T08:28:58Z","isPatch":true,"body":"In the preceding commits we have fixed recursion when creating the\nreference backends due to a chicken-and-egg situation with \"onbranch\"\nconditions. Unfortunately, this issue has existed for a while, and we\ndidn't really have a good mechanism to detect this recursion.\n\nImprove the status quo by detecting the recursion when creating the main\nreference store.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n refs.c | 7 +++++++\n 1 file changed, 7 insertions(+)\n\ndiff --git a/refs.c b/refs.c\nindex 5b773b1c15..1d24637891 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2359,15 +2359,22 @@ void ref_store_release(struct ref_store *ref_store)\n \n struct ref_store *get_main_ref_store(struct repository *r)\n {\n+\tstatic bool initializing;\n+\n \tif (r->refs_private)\n \t\treturn r->refs_private;\n \n \tif (!r->gitdir)\n \t\tBUG(\"attempting to get main_ref_store outside of repository\");\n+\tif (initializing)\n+\t\tBUG(\"initialization of main ref store is recursing\");\n \n+\tinitializing = true;\n \tr->refs_private = ref_store_init(r, r->ref_storage_format,\n \t\t\t\t\t r->gitdir, REF_STORE_ALL_CAPS);\n \tr->refs_private = maybe_debug_wrap_ref_store(r->gitdir, r->refs_private);\n+\tinitializing = false;\n+\n \treturn r->refs_private;\n }\n \n\n-- \n2.55.0.rc1.745.g43192e7977.dirty\n\n"},{"id":"546345","messageId":"ajxEXMTBmii01dVP@denethor","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-7-018475013dbc@pks.im","subject":"Re: [PATCH v5 07/11] refs: move parsing of \"core.logAllRefUpdates\" back into ref stores","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T21:22:07Z","receivedAt":"2026-06-24T21:22:11Z","isPatch":true,"body":"On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> In cc42c88945 (refs: extract out reflog config to generic layer,\n> 2026-05-04) we have refactored how we parse \"core.logAllRefUpdates\" so\n> that it happens in the generic layer. Unfortunately, this has worsened a\n> preexisting issue where we may recurse when creating the reference store\n> because of a chicken-and-egg problem between parsing the configuration\n> and evaluating \"onbranch\" conditions.\n\nOk so IIUC, parsing \"core.logAllRefUpdates\" in the generic layer forces\nus to read the config earlier. This is problematic though since the\nrefstore has not been initialized yet which we need to evaluate\n\"onbranch\" conditions.\n\n> Prepare for a fix by essentially reverting that change so that we handle\n> this setting in the respective backends again. The backends are already\n> parsing other configuration anyway, so by moving the logic back in there\n> we can ensure that all backend configuration is parsed the same way.\n\nMakes sense.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/checkout.c      |  7 +++++--\n>  refs.c                  | 10 +++++++++-\n>  refs.h                  |  9 +++++++++\n>  refs/files-backend.c    | 20 +++++++++++++++++---\n>  refs/refs-internal.h    |  6 ------\n>  refs/reftable-backend.c | 20 +++++++++++---------\n>  repo-settings.c         | 16 ----------------\n>  repo-settings.h         |  9 ---------\n>  setup.c                 |  7 ++++++-\n>  9 files changed, 57 insertions(+), 47 deletions(-)\n> \n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index b78b3a1d16..aee84ca897 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -952,10 +952,13 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n>  \tconst char *old_desc, *reflog_msg;\n>  \tif (opts->new_branch) {\n>  \t\tif (opts->new_orphan_branch) {\n> -\t\t\tenum log_refs_config log_all_ref_updates =\n> -\t\t\t\trepo_settings_get_log_all_ref_updates(the_repository);\n> +\t\t\tenum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;\n> +\t\t\tconst char *value;\n>  \t\t\tchar *refname;\n>  \n> +\t\t\tif (!repo_config_get_string_tmp(the_repository, \"core.logallrefupdates\", &value))\n> +\t\t\t\tlog_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n> +\n>  \t\t\trefname = mkpathdup(\"refs/heads/%s\", opts->new_orphan_branch);\n>  \t\t\tif (opts->new_branch_log &&\n>  \t\t\t    !should_autocreate_reflog(log_all_ref_updates, refname)) {\n> diff --git a/refs.c b/refs.c\n> index d3caa9a633..5b773b1c15 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -1053,6 +1053,15 @@ static char *normalize_reflog_message(const char *msg)\n>  \treturn strbuf_detach(&sb, NULL);\n>  }\n>  \n> +enum log_refs_config refs_parse_log_all_ref_updates_config(const char *value)\n> +{\n> +\tif (value && !strcasecmp(value, \"always\"))\n> +\t\treturn LOG_REFS_ALWAYS;\n> +\telse if (git_config_bool(\"core.logallrefupdates\", value))\n> +\t\treturn LOG_REFS_NORMAL;\n> +\treturn LOG_REFS_NONE;\n> +}\n\nThis function replaces `repo_settings_get_log_all_ref_updates()`. I\nassume we just wanted a slightly more simple function where the only\nconcern was parsing the `core.logallrefupdates` value.\n\n> +\n>  int should_autocreate_reflog(enum log_refs_config log_all_ref_updates,\n>  \t\t\t     const char *refname)\n>  {\n> @@ -2327,7 +2336,6 @@ static struct ref_store *ref_store_init(struct repository *repo,\n>  \tstruct ref_store *refs;\n>  \tstruct ref_store_init_options opts = {\n>  \t\t.access_flags = flags,\n> -\t\t.log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo),\n\nThis config is no longer handled in the generic layer.\n\n[snip]\n> diff --git a/setup.c b/setup.c\n> index 79125db565..0c6efb0560 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -2584,10 +2584,15 @@ static int create_default_files(struct repository *repo,\n>  \tif (is_bare_repository())\n>  \t\trepo_config_set(repo, \"core.bare\", \"true\");\n>  \telse {\n> +\t\tconst char *value;\n> +\n>  \t\trepo_config_set(repo, \"core.bare\", \"false\");\n> +\n>  \t\t/* allow template config file to override the default */\n> -\t\tif (repo_settings_get_log_all_ref_updates(repo) == LOG_REFS_UNSET)\n> +\t\tif (repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value) ||\n> +\t\t    refs_parse_log_all_ref_updates_config(value) == LOG_REFS_UNSET)\n\nHuh, can `refs_parse_log_all_ref_updates_config()` even return\nLOG_REFS_UNSET?\n\n-Justin\n"},{"id":"546346","messageId":"ajxKh-IrC2EPWJnW@denethor","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-8-018475013dbc@pks.im","subject":"Re: [PATCH v5 08/11] refs/files: lazy-load configuration to fix chicken-and-egg","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T21:36:28Z","receivedAt":"2026-06-24T21:36:32Z","isPatch":true,"body":"On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> When initializing the \"files\" reference backend we read the repository's\n> config to parse \"core.preferSymlinkRefs\" and \"core.logAllRefUpdates\".\n> This results in a chicken-and-egg problem though, because parsing the\n> configuration may require us to have access to the reference store\n> already when an \"onbranch\" condition exists.\n\nOk so both of these configuration options are currently parsed at ref\nstore initialization time. This is problematic because we need the ref\nstore to properly handle \"onbranch\" conditions in the config.\n\n> Luckily, all the configuration that we honor only relates to writing\n> references. Consequently, we don't strictly need that configuration to\n> be readily available at initialization time, and we can easiliy defer\n> parsing it to a later point in time.\n\nThat's nice. So we don't actually need this configuration during\ninitialization and can instead lazily load it when writing the first\nreferences. Makes sense.\n\n> Implement this fix and add tests that verify that we can indeed properly\n> parse these config knobs via an \"onbranch\" condition.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/files-backend.c        | 37 ++++++++++++++++++++++++++-----------\n>  t/t0600-reffiles-backend.sh | 21 +++++++++++++++++++++\n>  2 files changed, 47 insertions(+), 11 deletions(-)\n> \n> diff --git a/refs/files-backend.c b/refs/files-backend.c\n> index 79fb6735e1..d0f379dcd6 100644\n> --- a/refs/files-backend.c\n> +++ b/refs/files-backend.c\n> @@ -84,12 +84,14 @@ struct files_ref_store {\n>  \tunsigned int store_flags;\n>  \n>  \tchar *gitcommondir;\n> -\tenum log_refs_config log_all_ref_updates;\n> -\tint prefer_symlink_refs;\n> -\n>  \tstruct ref_cache *loose;\n> -\n>  \tstruct ref_store *packed_ref_store;\n> +\n> +\tstruct files_ref_store_write_options {\n> +\t\tenum log_refs_config log_all_ref_updates;\n> +\t\tint prefer_symlink_refs;\n> +\t\tbool initialized;\n> +\t} write_opts_lazy_loaded;\n\nIt might be nice to leave some sort of breadcrumb comment to future\nreaders to explain why we lazy load this configuration.\n\n>  };\n>  \n>  static void clear_loose_ref_cache(struct files_ref_store *refs)\n> @@ -121,17 +123,31 @@ static int files_ref_store_config(const char *var, const char *value,\n>  \t\t\t\t  const struct config_context *ctx UNUSED,\n>  \t\t\t\t  void *payload)\n>  {\n> -\tstruct files_ref_store *refs = payload;\n> +\tstruct files_ref_store_write_options *opts = payload;\n>  \n>  \tif (!strcmp(var, \"core.prefersymlinkrefs\")) {\n> -\t\trefs->prefer_symlink_refs = git_config_bool(var, value);\n> +\t\topts->prefer_symlink_refs = git_config_bool(var, value);\n>  \t} else if (!strcmp(var, \"core.logallrefupdates\")) {\n> -\t\trefs->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n> +\t\topts->log_all_ref_updates = refs_parse_log_all_ref_updates_config(value);\n>  \t}\n>  \n>  \treturn 0;\n>  }\n>  \n> +static const struct files_ref_store_write_options *files_ref_store_write_options(struct files_ref_store *refs)\n> +{\n> +\tstruct files_ref_store_write_options *opts = &refs->write_opts_lazy_loaded;\n> +\n> +\tif (opts->initialized)\n> +\t\treturn opts;\n> +\n> +\topts->log_all_ref_updates = LOG_REFS_UNSET;\n> +\trepo_config(refs->base.repo, files_ref_store_config, opts);\n> +\n> +\topts->initialized = true;\n> +\treturn opts;\n> +}\n> +\n>  /*\n>   * Create a new submodule ref cache and add it to the internal\n>   * set of caches.\n> @@ -156,9 +172,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo,\n>  \trefs->packed_ref_store =\n>  \t\tpacked_ref_store_init(repo, NULL, refs->gitcommondir, opts);\n>  \trefs->store_flags = opts->access_flags;\n> -\trefs->log_all_ref_updates = LOG_REFS_UNSET;\n>  \n> -\trepo_config(repo, files_ref_store_config, refs);\n\nConfigs are no longer read eagerly during initialization.\n\nThe rest of this patch looks good to me.\n\n-Justin\n"},{"id":"546366","messageId":"ajxR2fLRsIvNYFtz@denethor","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-9-018475013dbc@pks.im","subject":"Re: [PATCH v5 09/11] reftable: split up write options","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T22:06:32Z","receivedAt":"2026-06-24T22:06:36Z","isPatch":true,"body":"On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> When initializing the reftable stack the caller may optionally pass some\n> write options. These write options mix up two different concerns though:\n> \n>   - Of course, they allow the caller to configure how new reftables are\n>     being written.\n> \n>   - But they also allow the caller to configure the stack itself, like\n>     its hash ID and the `on_reload` callback.\n> \n> This is somewhat awkward, as it doesn't easily give the caller the\n> flexibility to for example write multiple reftables with different\n> options. Furthermore, this requires us to eagerly parse relevant\n> configuration when initializing the reftable backend.\n\nNaive question: are there any current use cases where callers may want\nto write multiple reftables with a different set of options? Can\nreftables written with different options pose any correctness issues?\n\n> Refactor the code by splitting out those options that configure the\n> stack itself. Creating a new stack will thus only require this limited\n> set of options, whereas the caller is expected to pass write options to\n> all functions that end up writing tables.\n\nSplitting this up sounds reasonable.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/reftable-backend.c             |  29 +++---\n>  reftable/reftable-stack.h           |  30 +++++-\n>  reftable/reftable-writer.h          |  17 +---\n>  reftable/stack.c                    | 100 ++++++++++++-------\n>  reftable/stack.h                    |   2 +-\n>  reftable/writer.c                   |  21 ++--\n>  reftable/writer.h                   |   1 +\n>  t/helper/test-reftable.c            |   2 +-\n>  t/unit-tests/lib-reftable.c         |   8 +-\n>  t/unit-tests/lib-reftable.h         |   2 +\n>  t/unit-tests/u-reftable-merged.c    |   9 +-\n>  t/unit-tests/u-reftable-readwrite.c |  38 ++++++--\n>  t/unit-tests/u-reftable-stack.c     | 189 ++++++++++++++++--------------------\n>  t/unit-tests/u-reftable-table.c     |   8 +-\n>  14 files changed, 258 insertions(+), 198 deletions(-)\n> \n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 5115a3f4ce..608d71cf10 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -48,9 +48,9 @@ static void reftable_backend_on_reload(void *payload)\n>  \n>  static int reftable_backend_init(struct reftable_backend *be,\n>  \t\t\t\t const char *path,\n> -\t\t\t\t const struct reftable_write_options *_opts)\n\nOk so now during init we only care about `struct\nreftable_stack_options`. The `struct reftable_write_options` are only\nneeded during reftable writes.\n\n[snip]\n> +/* Options related to opening a stack. */\n> +struct reftable_stack_options {\n> +\t/*\n> +\t * 4-byte identifier (\"sha1\", \"s256\") of the hash. Defaults to SHA1 if\n> +\t * unset.\n> +\t */\n> +\tenum reftable_hash hash_id;\n> +\n> +\t/*\n> +\t * Callback function to execute whenever the stack is being reloaded.\n> +\t * This can be used e.g. to discard cached information that relies on\n> +\t * the old stack's data. The payload data will be passed as argument to\n> +\t * the callback.\n> +\t */\n> +\tvoid (*on_reload)(void *payload);\n> +\tvoid *on_reload_payload;\n> +};\n\nThese are the options split out from `struct reftable_write_options` and\nare the options used at initialization and expected to remain consistent\nacross reftable writes. I assume these also won't depend on reading the\nconfig prior to the ref store being initialzed.\n\n[snip]\n> diff --git a/reftable/reftable-writer.h b/reftable/reftable-writer.h\n> index a66db415c8..6ff4ddfc60 100644\n> --- a/reftable/reftable-writer.h\n> +++ b/reftable/reftable-writer.h\n> @@ -28,11 +28,6 @@ struct reftable_write_options {\n>  \t/* how often to write complete keys in each block. */\n>  \tuint16_t restart_interval;\n>  \n> -\t/* 4-byte identifier (\"sha1\", \"s256\") of the hash.\n> -\t * Defaults to SHA1 if unset\n> -\t */\n> -\tenum reftable_hash hash_id;\n> -\n>  \t/* Default mode for creating files. If unset, use 0666 (+umask) */\n>  \tunsigned int default_permissions;\n>  \n> @@ -60,15 +55,6 @@ struct reftable_write_options {\n>  \t * negative value will cause us to block indefinitely.\n>  \t */\n>  \tlong lock_timeout_ms;\n> -\n> -\t/*\n> -\t * Callback function to execute whenever the stack is being reloaded.\n> -\t * This can be used e.g. to discard cached information that relies on\n> -\t * the old stack's data. The payload data will be passed as argument to\n> -\t * the callback.\n> -\t */\n> -\tvoid (*on_reload)(void *payload);\n> -\tvoid *on_reload_payload;\n>  };\n\nThese write options are explicitly passed around during write\noperations. I assume some of these options must be parsed from the\nconfig and thus will need to be lazy-loaded to avoid \"onbranch\"\nconditions prior to the ref store being initialzed.\n\nThe rest of this patch looks to be adjusting call sites to wire these\noptions through as needed and looks correct. I don't see any changes to\nlazy-load write option configuration yet, but I suppose that will happen\nin a subsequent patch.\n\n-Justin\n"},{"id":"546367","messageId":"ajxU-McoGrfkeKTs@denethor","threadId":"65786","inReplyTo":"20260622-b4-pks-refs-avoid-chdir-notify-reparent-v5-10-018475013dbc@pks.im","subject":"Re: [PATCH v5 10/11] refs/reftable: lazy-load configuration to fix chicken-and-egg","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T22:18:21Z","receivedAt":"2026-06-24T22:18:26Z","isPatch":true,"body":"On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> Same as with the \"files\" backend, the \"reftable\" backend also has a\n> chicken-and-egg problem with \"onbranch\" conditions. Fix this issue the\n> same as we did with the \"files\" backend by lazy-loading configuration.\n\nMakes sense.\n\n> Now that both the \"files\" and the \"reftable\" backend handle this\n> properly, add a generic test to t1400 that verifies that the user can\n> configure \"core.logAllRefUpdates\" via an \"onbranch\" condition. This is\n> mostly a nonsensical thing to do in the first place, but it serves as a\n> good sanity chekc.\n\ns/chekc/check\n\n> Note that we had to move `should_write_log()` around so that it can\n> access the new `reftable_be_write_options()` function.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  refs/reftable-backend.c           | 146 ++++++++++++++++++++++----------------\n>  t/t0613-reftable-write-options.sh |  19 +++++\n>  t/t1400-update-ref.sh             |  12 ++++\n>  3 files changed, 116 insertions(+), 61 deletions(-)\n> \n> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> index 608d71cf10..d74131a5ae 100644\n> --- a/refs/reftable-backend.c\n> +++ b/refs/reftable-backend.c\n> @@ -141,10 +141,21 @@ struct reftable_ref_store {\n>  \t */\n>  \tstruct strmap worktree_backends;\n>  \tstruct reftable_stack_options stack_options;\n> -\tstruct reftable_write_options write_options;\n> +\n> +\t/*\n> +\t * Options used when writing to or compacting the reftable stacks.\n> +\t * These are parsed from the configuration lazily on first use via\n> +\t * `reftable_be_write_options()` so that we don't have to access the\n> +\t * configuration when initializing the ref store. Do not access these\n> +\t * fields directly, but use the accessor instead.\n> +\t */\n> +\tstruct reftable_be_write_options {\n> +\t\tstruct reftable_write_options opts;\n> +\t\tenum log_refs_config log_all_ref_updates;\n\nAny reason in particular that `log_all_ref_updates` is the only option\noutside of `struct reftlable_write_options` here? Isn't it also only\nused during writes?\n\n-Justin\n"},{"id":"546375","messageId":"ajzMLEgOs7E9kiBK@pks.im","threadId":"65786","inReplyTo":"ajxEXMTBmii01dVP@denethor","subject":"Re: [PATCH v5 07/11] refs: move parsing of \"core.logAllRefUpdates\" back into ref stores","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-25T06:35:24Z","receivedAt":"2026-06-25T06:35:37Z","isPatch":true,"body":"On Wed, Jun 24, 2026 at 04:22:07PM -0500, Justin Tobler wrote:\n> On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> > diff --git a/setup.c b/setup.c\n> > index 79125db565..0c6efb0560 100644\n> > --- a/setup.c\n> > +++ b/setup.c\n> > @@ -2584,10 +2584,15 @@ static int create_default_files(struct repository *repo,\n> >  \tif (is_bare_repository())\n> >  \t\trepo_config_set(repo, \"core.bare\", \"true\");\n> >  \telse {\n> > +\t\tconst char *value;\n> > +\n> >  \t\trepo_config_set(repo, \"core.bare\", \"false\");\n> > +\n> >  \t\t/* allow template config file to override the default */\n> > -\t\tif (repo_settings_get_log_all_ref_updates(repo) == LOG_REFS_UNSET)\n> > +\t\tif (repo_config_get_string_tmp(repo, \"core.logallrefupdates\", &value) ||\n> > +\t\t    refs_parse_log_all_ref_updates_config(value) == LOG_REFS_UNSET)\n> \n> Huh, can `refs_parse_log_all_ref_updates_config()` even return\n> LOG_REFS_UNSET?\n\nIt can't, so the second statement is really redundant. All that we care\nabout there is that the configuration isn't already set, which is\nalready covered by the first statement.\n\nWill adapt.\n\nPatrick\n"},{"id":"546376","messageId":"ajzMOViv2sIbLzS0@pks.im","threadId":"65786","inReplyTo":"ajxKh-IrC2EPWJnW@denethor","subject":"Re: [PATCH v5 08/11] refs/files: lazy-load configuration to fix chicken-and-egg","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-25T06:35:37Z","receivedAt":"2026-06-25T06:35:42Z","isPatch":true,"body":"On Wed, Jun 24, 2026 at 04:36:28PM -0500, Justin Tobler wrote:\n> On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> > diff --git a/refs/files-backend.c b/refs/files-backend.c\n> > index 79fb6735e1..d0f379dcd6 100644\n> > --- a/refs/files-backend.c\n> > +++ b/refs/files-backend.c\n> > @@ -84,12 +84,14 @@ struct files_ref_store {\n> >  \tunsigned int store_flags;\n> >  \n> >  \tchar *gitcommondir;\n> > -\tenum log_refs_config log_all_ref_updates;\n> > -\tint prefer_symlink_refs;\n> > -\n> >  \tstruct ref_cache *loose;\n> > -\n> >  \tstruct ref_store *packed_ref_store;\n> > +\n> > +\tstruct files_ref_store_write_options {\n> > +\t\tenum log_refs_config log_all_ref_updates;\n> > +\t\tint prefer_symlink_refs;\n> > +\t\tbool initialized;\n> > +\t} write_opts_lazy_loaded;\n> \n> It might be nice to leave some sort of breadcrumb comment to future\n> readers to explain why we lazy load this configuration.\n\nFair, will do.\n\nPatrick\n"},{"id":"546377","messageId":"ajzMTTW_UzTvldN1@pks.im","threadId":"65786","inReplyTo":"ajxR2fLRsIvNYFtz@denethor","subject":"Re: [PATCH v5 09/11] reftable: split up write options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-25T06:35:57Z","receivedAt":"2026-06-25T06:36:04Z","isPatch":true,"body":"On Wed, Jun 24, 2026 at 05:06:32PM -0500, Justin Tobler wrote:\n> On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> > When initializing the reftable stack the caller may optionally pass some\n> > write options. These write options mix up two different concerns though:\n> > \n> >   - Of course, they allow the caller to configure how new reftables are\n> >     being written.\n> > \n> >   - But they also allow the caller to configure the stack itself, like\n> >     its hash ID and the `on_reload` callback.\n> > \n> > This is somewhat awkward, as it doesn't easily give the caller the\n> > flexibility to for example write multiple reftables with different\n> > options. Furthermore, this requires us to eagerly parse relevant\n> > configuration when initializing the reftable backend.\n> \n> Naive question: are there any current use cases where callers may want\n> to write multiple reftables with a different set of options? Can\n> reftables written with different options pose any correctness issues?\n\nThere aren't, but in theory it's totally fine to do it. One could for\nexample imagine that a large reference transaction wants to use a larger\nblock size with a different restart interval.\n\nThe only thing that of course shouldn't happen is that the different\ntables use different hashes.\n\nPatrick\n"},{"id":"546378","messageId":"ajzMVbyfkNF_LEgX@pks.im","threadId":"65786","inReplyTo":"ajxU-McoGrfkeKTs@denethor","subject":"Re: [PATCH v5 10/11] refs/reftable: lazy-load configuration to fix chicken-and-egg","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-25T06:36:05Z","receivedAt":"2026-06-25T06:36:09Z","isPatch":true,"body":"On Wed, Jun 24, 2026 at 05:18:21PM -0500, Justin Tobler wrote:\n> On 26/06/22 10:28AM, Patrick Steinhardt wrote:\n> > diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c\n> > index 608d71cf10..d74131a5ae 100644\n> > --- a/refs/reftable-backend.c\n> > +++ b/refs/reftable-backend.c\n> > @@ -141,10 +141,21 @@ struct reftable_ref_store {\n> >  \t */\n> >  \tstruct strmap worktree_backends;\n> >  \tstruct reftable_stack_options stack_options;\n> > -\tstruct reftable_write_options write_options;\n> > +\n> > +\t/*\n> > +\t * Options used when writing to or compacting the reftable stacks.\n> > +\t * These are parsed from the configuration lazily on first use via\n> > +\t * `reftable_be_write_options()` so that we don't have to access the\n> > +\t * configuration when initializing the ref store. Do not access these\n> > +\t * fields directly, but use the accessor instead.\n> > +\t */\n> > +\tstruct reftable_be_write_options {\n> > +\t\tstruct reftable_write_options opts;\n> > +\t\tenum log_refs_config log_all_ref_updates;\n> \n> Any reason in particular that `log_all_ref_updates` is the only option\n> outside of `struct reftlable_write_options` here? Isn't it also only\n> used during writes?\n\n`log_all_ref_updates` is part of the backend's logic, whereas the\n`struct reftable_write_options` is part of the reftable library's logic.\nSo they have different scopes, and the former cannot be handled in the\nlibrary.\n\nPatrick\n"}]}