From d4c633c7af414db515191743224ad2dd3706ee51 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:36 +0100 Subject: [PATCH 1/6] setup: don't modify repo in `create_reference_database()` The `create_reference_database()` function is used to create the reference database during initialization of a repository. The function calls `repo_set_ref_storage_format()` to set the repositories reference format. This is an unexpected side-effect of the function. More so because the function is only called in two locations: 1. During git-init(1) where the value is propagated from the `struct repository_format repo_fmt` value. 2. During git-clone(1) where the value is propagated from the `the_repository` value. The former is valid, however the flow already calls `repo_set_ref_storage_format()`, so this effort is simply duplicated. The latter sets the existing value in `the_repository` back to itself. While this is okay for now, introduction of more fields in `repo_set_ref_storage_format()` would cause issues, especially dynamically allocated strings, where we would free/allocate the same string back into `the_repostiory`. To avoid all this confusion, clean up the function to no longer take in and set the repo's reference storage format. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/clone.c | 2 +- setup.c | 7 ++----- setup.h | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index b40cee5968..cd43bb5aa2 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1442,7 +1442,7 @@ int cmd_clone(int argc, hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport)); initialize_repository_version(hash_algo, the_repository->ref_storage_format, 1); repo_set_hash_algo(the_repository, hash_algo); - create_reference_database(the_repository->ref_storage_format, NULL, 1); + create_reference_database(NULL, 1); /* * Before fetching from the remote, download and install bundle diff --git a/setup.c b/setup.c index b723f8b339..1fc9ae3872 100644 --- a/setup.c +++ b/setup.c @@ -2359,14 +2359,12 @@ static int is_reinit(void) return ret; } -void create_reference_database(enum ref_storage_format ref_storage_format, - const char *initial_branch, int quiet) +void create_reference_database(const char *initial_branch, int quiet) { struct strbuf err = STRBUF_INIT; char *to_free = NULL; int reinit = is_reinit(); - repo_set_ref_storage_format(the_repository, ref_storage_format); if (ref_store_create_on_disk(get_main_ref_store(the_repository), 0, &err)) die("failed to set up refs db: %s", err.buf); @@ -2701,8 +2699,7 @@ int init_db(const char *git_dir, const char *real_git_dir, &repo_fmt, init_shared_repository); if (!(flags & INIT_DB_SKIP_REFDB)) - create_reference_database(repo_fmt.ref_storage_format, - initial_branch, flags & INIT_DB_QUIET); + create_reference_database(initial_branch, flags & INIT_DB_QUIET); create_object_directory(); if (repo_settings_get_shared_repository(the_repository)) { diff --git a/setup.h b/setup.h index d55dcc6608..ddb9f6701c 100644 --- a/setup.h +++ b/setup.h @@ -240,8 +240,7 @@ int init_db(const char *git_dir, const char *real_git_dir, void initialize_repository_version(int hash_algo, enum ref_storage_format ref_storage_format, int reinit); -void create_reference_database(enum ref_storage_format ref_storage_format, - const char *initial_branch, int quiet); +void create_reference_database(const char *initial_branch, int quiet); /* * NOTE NOTE NOTE!! From 46bae534c9525890ab52f2c4ce7eaf628b55cd13 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:37 +0100 Subject: [PATCH 2/6] refs: extract out `refs_create_refdir_stubs()` For Git to recognize a directory as a Git directory, it requires the directory to contain: 1. 'HEAD' file 2. 'objects/' directory 3. 'refs/' directory Here, #1 and #3 are part of the reference storage mechanism, specifically the files backend. Since then, newer backends such as the reftable backend have moved to using their own path ('reftable/') for storing references. But to ensure Git still recognizes the directory as a Git directory, we create stubs. There are two locations where we create stubs: - In 'refs/reftable-backend.c' when creating the reftable backend. - In 'clone.c' before spawning transport helpers. In a following commit, we'll add another instance. So instead of repeating the code, let's extract out this code to `refs_create_refdir_stubs()` and use it. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/clone.c | 7 +------ refs.c | 23 +++++++++++++++++++++++ refs.h | 13 +++++++++++++ refs/reftable-backend.c | 14 ++------------ 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index cd43bb5aa2..697c5bb5cb 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1225,12 +1225,7 @@ int cmd_clone(int argc, initialize_repository_version(GIT_HASH_UNKNOWN, the_repository->ref_storage_format, 1); - strbuf_addf(&buf, "%s/HEAD", git_dir); - write_file(buf.buf, "ref: refs/heads/.invalid"); - - strbuf_reset(&buf); - strbuf_addf(&buf, "%s/refs", git_dir); - safe_create_dir(the_repository, buf.buf, 1); + refs_create_refdir_stubs(the_repository, git_dir, NULL); /* * additional config can be injected with -c, make sure it's included diff --git a/refs.c b/refs.c index 627b7f8698..77b93d655b 100644 --- a/refs.c +++ b/refs.c @@ -2163,6 +2163,29 @@ const char *refs_resolve_ref_unsafe(struct ref_store *refs, return NULL; } +void refs_create_refdir_stubs(struct repository *repo, const char *refdir, + const char *refs_heads_content) +{ + struct strbuf path = STRBUF_INIT; + + strbuf_addf(&path, "%s/HEAD", refdir); + write_file(path.buf, "ref: refs/heads/.invalid"); + adjust_shared_perm(repo, path.buf); + + strbuf_reset(&path); + strbuf_addf(&path, "%s/refs", refdir); + safe_create_dir(repo, path.buf, 1); + + if (refs_heads_content) { + strbuf_reset(&path); + strbuf_addf(&path, "%s/refs/heads", refdir); + write_file(path.buf, "%s", refs_heads_content); + adjust_shared_perm(repo, path.buf); + } + + strbuf_release(&path); +} + /* backend functions */ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *err) { diff --git a/refs.h b/refs.h index f0abfa1d93..9d8890fdff 100644 --- a/refs.h +++ b/refs.h @@ -1427,4 +1427,17 @@ void ref_iterator_free(struct ref_iterator *ref_iterator); int do_for_each_ref_iterator(struct ref_iterator *iter, each_ref_fn fn, void *cb_data); +/* + * Git only recognizes a directory as a repository if it contains: + * - HEAD file + * - refs/ folder + * While it is necessary within the files backend, newer backends may not + * follow the same structure. To go around this, we create stubs as necessary. + * + * If provided with a 'refs_heads_msg', we create the 'refs/heads/head' file + * with the provided message. + */ +void refs_create_refdir_stubs(struct repository *repo, const char *refdir, + const char *refs_heads_msg); + #endif /* REFS_H */ diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index fe74af73af..d8651fe779 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -491,18 +491,8 @@ static int reftable_be_create_on_disk(struct ref_store *ref_store, safe_create_dir(the_repository, sb.buf, 1); strbuf_reset(&sb); - strbuf_addf(&sb, "%s/HEAD", refs->base.gitdir); - write_file(sb.buf, "ref: refs/heads/.invalid"); - adjust_shared_perm(the_repository, sb.buf); - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs", refs->base.gitdir); - safe_create_dir(the_repository, sb.buf, 1); - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs/heads", refs->base.gitdir); - write_file(sb.buf, "this repository uses the reftable format"); - adjust_shared_perm(the_repository, sb.buf); + refs_create_refdir_stubs(the_repository, refs->base.gitdir, + "this repository uses the reftable format"); strbuf_release(&sb); return 0; From bb200c1d170a0d5857c1c4096e44fea965abbb40 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:38 +0100 Subject: [PATCH 3/6] refs: move out stub modification to generic layer When creating the reftable reference backend on disk, we create stubs to ensure that the directory can be recognized as a Git repository. This is done by calling `refs_create_refdir_stubs()`. Move this to the generic layer as this is needed for all backends excluding from the files backends. In an upcoming commit where we introduce alternate reference backend locations, we'll have to also create stubs in the $GIT_DIR irrespective of the backend being used. This commit builds the base to add that logic. Similarly, move the logic for deletion of stubs to the generic layer. The files backend recursively calls the remove function of the 'packed-backend', here skip calling the generic function since that would try to delete stubs. Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- refs.c | 47 +++++++++++++++++++++++++++++++++++++++-- refs/files-backend.c | 6 +++++- refs/reftable-backend.c | 27 ----------------------- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/refs.c b/refs.c index 77b93d655b..c83af63dc5 100644 --- a/refs.c +++ b/refs.c @@ -2189,12 +2189,55 @@ void refs_create_refdir_stubs(struct repository *repo, const char *refdir, /* backend functions */ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *err) { - return refs->be->create_on_disk(refs, flags, err); + int ret = refs->be->create_on_disk(refs, flags, err); + + if (!ret && + ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf msg = STRBUF_INIT; + + strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); + refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); + strbuf_release(&msg); + } + + return ret; } int ref_store_remove_on_disk(struct ref_store *refs, struct strbuf *err) { - return refs->be->remove_on_disk(refs, err); + int ret = refs->be->remove_on_disk(refs, err); + + if (!ret && + ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf sb = STRBUF_INIT; + + strbuf_addf(&sb, "%s/HEAD", refs->gitdir); + if (unlink(sb.buf) < 0) { + strbuf_addf(err, "could not delete stub HEAD: %s", + strerror(errno)); + ret = -1; + } + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs/heads", refs->gitdir); + if (unlink(sb.buf) < 0) { + strbuf_addf(err, "could not delete stub heads: %s", + strerror(errno)); + ret = -1; + } + strbuf_reset(&sb); + + strbuf_addf(&sb, "%s/refs", refs->gitdir); + if (rmdir(sb.buf) < 0) { + strbuf_addf(err, "could not delete refs directory: %s", + strerror(errno)); + ret = -1; + } + + strbuf_release(&sb); + } + + return ret; } int repo_resolve_gitlink_ref(struct repository *r, diff --git a/refs/files-backend.c b/refs/files-backend.c index 240d3c3b26..d3f6423261 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -3700,7 +3700,11 @@ static int files_ref_store_remove_on_disk(struct ref_store *ref_store, if (for_each_root_ref(refs, remove_one_root_ref, &data) < 0) ret = -1; - if (ref_store_remove_on_disk(refs->packed_ref_store, err) < 0) + /* + * Directly access the cleanup functions for packed-refs as the generic function + * would try to clear stubs which isn't required for the files backend. + */ + if (refs->packed_ref_store->be->remove_on_disk(refs->packed_ref_store, err) < 0) ret = -1; strbuf_release(&sb); diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index d8651fe779..6ce7f9bb8e 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -491,9 +491,6 @@ static int reftable_be_create_on_disk(struct ref_store *ref_store, safe_create_dir(the_repository, sb.buf, 1); strbuf_reset(&sb); - refs_create_refdir_stubs(the_repository, refs->base.gitdir, - "this repository uses the reftable format"); - strbuf_release(&sb); return 0; } @@ -519,30 +516,6 @@ static int reftable_be_remove_on_disk(struct ref_store *ref_store, strerror(errno)); ret = -1; } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/HEAD", refs->base.gitdir); - if (unlink(sb.buf) < 0) { - strbuf_addf(err, "could not delete stub HEAD: %s", - strerror(errno)); - ret = -1; - } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs/heads", refs->base.gitdir); - if (unlink(sb.buf) < 0) { - strbuf_addf(err, "could not delete stub heads: %s", - strerror(errno)); - ret = -1; - } - strbuf_reset(&sb); - - strbuf_addf(&sb, "%s/refs", refs->base.gitdir); - if (rmdir(sb.buf) < 0) { - strbuf_addf(err, "could not delete refs directory: %s", - strerror(errno)); - ret = -1; - } strbuf_release(&sb); return ret; From 6f9bc8ff28204a944f1e3e15a24ae75436682105 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:39 +0100 Subject: [PATCH 4/6] refs: receive and use the reference storage payload An upcoming commit will add support for providing an URI via the 'extensions.refStorage' config. The URI will contain the reference backend and a corresponding payload. The payload can be then used for providing an alternate locations for the reference backend. To prepare for this, modify the existing backends to accept such an argument when initializing via the 'init()' function. Both the files and reftable backends will parse the information to be filesystem paths to store references. Given that no callers pass any payload yet this is essentially a no-op change for now. To enable this, provide a 'refs_compute_filesystem_location()' function which will parse the current 'gitdir' and the 'payload' to provide the final reference directory and common reference directory (if working in a linked worktree). The documentation and tests will be added alongside the extension of the config variable. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- refs.c | 40 +++++++++++++++++++++++++++++++++++++++- refs/files-backend.c | 17 ++++++++++++----- refs/packed-backend.c | 5 +++++ refs/packed-backend.h | 1 + refs/refs-internal.h | 14 ++++++++++++++ refs/reftable-backend.c | 24 ++++++++++++++---------- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/refs.c b/refs.c index c83af63dc5..ba2573eb7a 100644 --- a/refs.c +++ b/refs.c @@ -5,6 +5,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "abspath.h" #include "advice.h" #include "config.h" #include "environment.h" @@ -2290,7 +2291,7 @@ static struct ref_store *ref_store_init(struct repository *repo, if (!be) BUG("reference backend is unknown"); - refs = be->init(repo, gitdir, flags); + refs = be->init(repo, NULL, gitdir, flags); return refs; } @@ -3468,3 +3469,40 @@ const char *ref_transaction_error_msg(enum ref_transaction_error err) return "unknown failure"; } } + +void refs_compute_filesystem_location(const char *gitdir, const char *payload, + bool *is_worktree, struct strbuf *refdir, + struct strbuf *ref_common_dir) +{ + struct strbuf sb = STRBUF_INIT; + + *is_worktree = get_common_dir_noenv(ref_common_dir, gitdir); + + if (!payload) { + /* + * We can use the 'gitdir' as the 'refdir' without appending the + * worktree path, as the 'gitdir' here is already the worktree + * path and is different from 'commondir' denoted by 'ref_common_dir'. + */ + strbuf_addstr(refdir, gitdir); + return; + } + + if (!is_absolute_path(payload)) { + strbuf_addf(&sb, "%s/%s", ref_common_dir->buf, payload); + strbuf_realpath(ref_common_dir, sb.buf, 1); + } else { + strbuf_realpath(ref_common_dir, payload, 1); + } + + strbuf_addbuf(refdir, ref_common_dir); + + if (*is_worktree) { + const char *wt_id = strrchr(gitdir, '/'); + if (!wt_id) + BUG("worktree path does not contain slash"); + strbuf_addf(refdir, "/worktrees/%s", wt_id + 1); + } + + strbuf_release(&sb); +} diff --git a/refs/files-backend.c b/refs/files-backend.c index d3f6423261..9cde3ba724 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -106,19 +106,24 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) * set of caches. */ static struct ref_store *files_ref_store_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int flags) { struct files_ref_store *refs = xcalloc(1, sizeof(*refs)); struct ref_store *ref_store = (struct ref_store *)refs; - struct strbuf sb = STRBUF_INIT; + struct strbuf ref_common_dir = STRBUF_INIT; + struct strbuf refdir = STRBUF_INIT; + bool is_worktree; - base_ref_store_init(ref_store, repo, gitdir, &refs_be_files); + refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir, + &ref_common_dir); + + base_ref_store_init(ref_store, repo, refdir.buf, &refs_be_files); refs->store_flags = flags; - get_common_dir_noenv(&sb, gitdir); - refs->gitcommondir = strbuf_detach(&sb, NULL); + refs->gitcommondir = strbuf_detach(&ref_common_dir, NULL); refs->packed_ref_store = - packed_ref_store_init(repo, refs->gitcommondir, flags); + packed_ref_store_init(repo, NULL, refs->gitcommondir, flags); refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo); repo_config_get_bool(repo, "core.prefersymlinkrefs", &refs->prefer_symlink_refs); @@ -126,6 +131,8 @@ static struct ref_store *files_ref_store_init(struct repository *repo, chdir_notify_reparent("files-backend $GIT_COMMONDIR", &refs->gitcommondir); + strbuf_release(&refdir); + return ref_store; } diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 4ea0c12299..e7bb9f10f9 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -211,7 +211,12 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } +/* + * Since packed-refs is only stored in the common dir, don't parse the + * payload and rely on the files-backend to set 'gitdir' correctly. + */ struct ref_store *packed_ref_store_init(struct repository *repo, + const char *payload UNUSED, const char *gitdir, unsigned int store_flags) { diff --git a/refs/packed-backend.h b/refs/packed-backend.h index 9481d5e7c2..2c2377a356 100644 --- a/refs/packed-backend.h +++ b/refs/packed-backend.h @@ -14,6 +14,7 @@ struct ref_transaction; */ struct ref_store *packed_ref_store_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int store_flags); diff --git a/refs/refs-internal.h b/refs/refs-internal.h index c7d2a6e50b..4fb8fdb872 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -389,6 +389,7 @@ struct ref_store; * the ref_store and to record the ref_store for later lookup. */ typedef struct ref_store *ref_store_init_fn(struct repository *repo, + const char *payload, const char *gitdir, unsigned int flags); /* @@ -666,4 +667,17 @@ enum ref_transaction_error refs_verify_refnames_available(struct ref_store *refs unsigned int initial_transaction, struct strbuf *err); +/* + * Given a gitdir and the reference storage payload provided, retrieve the + * 'refdir' and 'ref_common_dir'. The former is where references should be + * stored for the current worktree, the latter is the common reference + * directory if working with a linked worktree. If working with the main + * worktree, both values will be the same. + * + * This is used by backends that store references in the repository directly. + */ +void refs_compute_filesystem_location(const char *gitdir, const char *payload, + bool *is_worktree, struct strbuf *refdir, + struct strbuf *ref_common_dir); + #endif /* REFS_REFS_INTERNAL_H */ diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 6ce7f9bb8e..0e220d6bb5 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -372,18 +372,24 @@ static int reftable_be_fsync(int fd) } static struct ref_store *reftable_be_init(struct repository *repo, + const char *payload, const char *gitdir, unsigned int store_flags) { struct reftable_ref_store *refs = xcalloc(1, sizeof(*refs)); + struct strbuf ref_common_dir = STRBUF_INIT; + struct strbuf refdir = STRBUF_INIT; struct strbuf path = STRBUF_INIT; - int is_worktree; + bool is_worktree; mode_t mask; mask = umask(0); umask(mask); - base_ref_store_init(&refs->base, repo, gitdir, &refs_be_reftable); + refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir, + &ref_common_dir); + + base_ref_store_init(&refs->base, repo, refdir.buf, &refs_be_reftable); strmap_init(&refs->worktree_backends); refs->store_flags = store_flags; refs->log_all_ref_updates = repo_settings_get_log_all_ref_updates(repo); @@ -419,14 +425,11 @@ static struct ref_store *reftable_be_init(struct repository *repo, /* * Set up the main reftable stack that is hosted in GIT_COMMON_DIR. * This stack contains both the shared and the main worktree refs. - * - * Note that we don't try to resolve the path in case we have a - * worktree because `get_common_dir_noenv()` already does it for us. */ - is_worktree = get_common_dir_noenv(&path, gitdir); + strbuf_addbuf(&path, &ref_common_dir); if (!is_worktree) { strbuf_reset(&path); - strbuf_realpath(&path, gitdir, 0); + strbuf_realpath(&path, ref_common_dir.buf, 0); } strbuf_addstr(&path, "/reftable"); refs->err = reftable_backend_init(&refs->main_backend, path.buf, @@ -443,10 +446,9 @@ static struct ref_store *reftable_be_init(struct repository *repo, * do it efficiently. */ if (is_worktree) { - strbuf_reset(&path); - strbuf_addf(&path, "%s/reftable", gitdir); + strbuf_addstr(&refdir, "/reftable"); - refs->err = reftable_backend_init(&refs->worktree_backend, path.buf, + refs->err = reftable_backend_init(&refs->worktree_backend, refdir.buf, &refs->write_options); if (refs->err) goto done; @@ -456,6 +458,8 @@ static struct ref_store *reftable_be_init(struct repository *repo, done: assert(refs->err != REFTABLE_API_ERROR); + strbuf_release(&ref_common_dir); + strbuf_release(&refdir); strbuf_release(&path); return &refs->base; } From 880e2b6b1603f53109096c841c669c4cb59257a9 Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:40 +0100 Subject: [PATCH 5/6] refs: allow reference location in refstorage config The 'extensions.refStorage' config is used to specify the reference backend for a given repository. Both the 'files' and 'reftable' backends utilize the $GIT_DIR as the reference folder by default in `get_main_ref_store()`. Since the reference backends are pluggable, this means that they could work with out-of-tree reference directories too. Extend the 'refStorage' config to also support taking an URI input, where users can specify the reference backend and the location. Add the required changes to obtain and propagate this value to the individual backends. Add the necessary documentation and tests. Traditionally, for linked worktrees, references were stored in the '$GIT_DIR/worktrees/' path. But when using an alternate reference storage path, it doesn't make sense to store the main worktree references in the new path, and the linked worktree references in the $GIT_DIR. So, let's store linked worktree references in '$ALTERNATE_REFERENCE_DIR/worktrees/'. To do this, create the necessary files and folders while also adding stubs in the $GIT_DIR path to ensure that it is still considered a Git directory. Ideally, we would want to pass in a `struct worktree *` to individual backends, instead of passing the `gitdir`. This allows them to handle worktree specific logic. Currently, that is not possible since the worktree code is: - Tied to using the global `the_repository` variable. - Is not setup before the reference database during initialization of the repository. Add a TODO in 'refs.c' to ensure we can eventually make that change. Helped-by: Patrick Steinhardt Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/config/extensions.adoc | 16 ++- builtin/worktree.c | 34 ++++++ refs.c | 6 +- repository.c | 9 +- repository.h | 8 +- setup.c | 34 +++++- setup.h | 1 + t/meson.build | 1 + t/t1423-ref-backend.sh | 159 +++++++++++++++++++++++++++ 9 files changed, 259 insertions(+), 9 deletions(-) create mode 100755 t/t1423-ref-backend.sh diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc index 532456644b..3e51da36d3 100644 --- a/Documentation/config/extensions.adoc +++ b/Documentation/config/extensions.adoc @@ -57,10 +57,24 @@ For historical reasons, this extension is respected regardless of the `core.repositoryFormatVersion` setting. refStorage::: - Specify the ref storage format to use. The acceptable values are: + Specify the ref storage format and a corresponding payload. The value + can be either a format name or a URI: + -- +* A format name alone (e.g., `reftable` or `files`). + +* A URI format `://` explicitly specifies both the + format and payload (e.g., `reftable:///foo/bar`). + +Supported format names are: ++ include::../ref-storage-format.adoc[] ++ +The payload is passed directly to the reference backend. For the files and +reftable backends, this must be a filesystem path where the references will +be stored. Defaulting to the commondir when no payload is provided. Relative +paths are resolved relative to the $GIT_DIR. Future backends may support +other payload schemes, e.g., postgres://127.0.0.1:5432?database=myrepo. -- + Note that this setting should only be set by linkgit:git-init[1] or diff --git a/builtin/worktree.c b/builtin/worktree.c index fbdaf2eb2e..293e808379 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -425,6 +425,39 @@ static int make_worktree_orphan(const char * ref, const struct add_opts *opts, return run_command(&cp); } +/* + * References for worktrees are generally stored in '$GIT_DIR/worktrees/'. + * But when using alternate reference directories, we want to store the worktree + * references in '$ALTERNATE_REFERENCE_DIR/worktrees/'. + * + * Create the necessary folder structure to facilitate the same. But to ensure + * that the former path is still considered a Git directory, add stubs. + */ +static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_path) +{ + struct strbuf sb = STRBUF_INIT; + char *path; + + path = wt->repo->ref_storage_payload; + if (!path) + return; + + if (!is_absolute_path(path)) + strbuf_addf(&sb, "%s/", wt->repo->commondir); + + strbuf_addf(&sb, "%s/worktrees", path); + safe_create_dir(wt->repo, sb.buf, 1); + strbuf_addf(&sb, "/%s", wt->id); + safe_create_dir(wt->repo, sb.buf, 1); + strbuf_reset(&sb); + + strbuf_addf(&sb, "this worktree stores references in %s/worktrees/%s", + path, wt->id); + refs_create_refdir_stubs(wt->repo, wt_git_path, sb.buf); + + strbuf_release(&sb); +} + static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { @@ -518,6 +551,7 @@ static int add_worktree(const char *path, const char *refname, ret = error(_("could not find created worktree '%s'"), name); goto done; } + setup_alternate_ref_dir(wt, sb_repo.buf); wt_refs = get_worktree_ref_store(wt); ret = ref_store_create_on_disk(wt_refs, REF_STORE_CREATE_ON_DISK_IS_WORKTREE, &sb); diff --git a/refs.c b/refs.c index ba2573eb7a..ef1902e85c 100644 --- a/refs.c +++ b/refs.c @@ -2291,7 +2291,11 @@ static struct ref_store *ref_store_init(struct repository *repo, if (!be) BUG("reference backend is unknown"); - refs = be->init(repo, NULL, gitdir, flags); + /* + * TODO Send in a 'struct worktree' instead of a 'gitdir', and + * allow the backend to handle how it wants to deal with worktrees. + */ + refs = be->init(repo, repo->ref_storage_payload, gitdir, flags); return refs; } diff --git a/repository.c b/repository.c index c7e75215ac..9815f081ef 100644 --- a/repository.c +++ b/repository.c @@ -193,9 +193,12 @@ void repo_set_compat_hash_algo(struct repository *repo, int algo) } void repo_set_ref_storage_format(struct repository *repo, - enum ref_storage_format format) + enum ref_storage_format format, + const char *payload) { repo->ref_storage_format = format; + free(repo->ref_storage_payload); + repo->ref_storage_payload = xstrdup_or_null(payload); } /* @@ -277,7 +280,8 @@ int repo_init(struct repository *repo, repo_set_hash_algo(repo, format.hash_algo); repo_set_compat_hash_algo(repo, format.compat_hash_algo); - repo_set_ref_storage_format(repo, format.ref_storage_format); + repo_set_ref_storage_format(repo, format.ref_storage_format, + format.ref_storage_payload); repo->repository_format_worktree_config = format.worktree_config; repo->repository_format_relative_worktrees = format.relative_worktrees; repo->repository_format_precious_objects = format.precious_objects; @@ -369,6 +373,7 @@ void repo_clear(struct repository *repo) FREE_AND_NULL(repo->index_file); FREE_AND_NULL(repo->worktree); FREE_AND_NULL(repo->submodule_prefix); + FREE_AND_NULL(repo->ref_storage_payload); odb_free(repo->objects); repo->objects = NULL; diff --git a/repository.h b/repository.h index 6063c4b846..95e2333bad 100644 --- a/repository.h +++ b/repository.h @@ -150,6 +150,11 @@ struct repository { /* Repository's reference storage format, as serialized on disk. */ enum ref_storage_format ref_storage_format; + /* + * Reference storage information as needed for the backend. This contains + * only the payload from the reference URI without the schema. + */ + char *ref_storage_payload; /* A unique-id for tracing purposes. */ int trace2_repo_id; @@ -204,7 +209,8 @@ void repo_set_worktree(struct repository *repo, const char *path); void repo_set_hash_algo(struct repository *repo, int algo); void repo_set_compat_hash_algo(struct repository *repo, int compat_algo); void repo_set_ref_storage_format(struct repository *repo, - enum ref_storage_format format); + enum ref_storage_format format, + const char *payload); void initialize_repository(struct repository *repo); RESULT_MUST_BE_USED int repo_init(struct repository *r, const char *gitdir, const char *worktree); diff --git a/setup.c b/setup.c index 1fc9ae3872..d407f3347b 100644 --- a/setup.c +++ b/setup.c @@ -632,6 +632,21 @@ static enum extension_result handle_extension_v0(const char *var, return EXTENSION_UNKNOWN; } +static void parse_reference_uri(const char *value, char **format, + char **payload) +{ + const char *schema_end; + + schema_end = strstr(value, "://"); + if (!schema_end) { + *format = xstrdup(value); + *payload = NULL; + } else { + *format = xstrndup(value, schema_end - value); + *payload = xstrdup_or_null(schema_end + 3); + } +} + /* * Record any new extensions in this function. */ @@ -674,10 +689,17 @@ static enum extension_result handle_extension(const char *var, return EXTENSION_OK; } else if (!strcmp(ext, "refstorage")) { unsigned int format; + char *format_str; if (!value) return config_error_nonbool(var); - format = ref_storage_format_by_name(value); + + parse_reference_uri(value, &format_str, + &data->ref_storage_payload); + + format = ref_storage_format_by_name(format_str); + free(format_str); + if (format == REF_STORAGE_FORMAT_UNKNOWN) return error(_("invalid value for '%s': '%s'"), "extensions.refstorage", value); @@ -850,6 +872,7 @@ void clear_repository_format(struct repository_format *format) string_list_clear(&format->v1_only_extensions, 0); free(format->work_tree); free(format->partial_clone); + free(format->ref_storage_payload); init_repository_format(format); } @@ -1942,7 +1965,8 @@ const char *setup_git_directory_gently(int *nongit_ok) repo_set_compat_hash_algo(the_repository, repo_fmt.compat_hash_algo); repo_set_ref_storage_format(the_repository, - repo_fmt.ref_storage_format); + repo_fmt.ref_storage_format, + repo_fmt.ref_storage_payload); the_repository->repository_format_worktree_config = repo_fmt.worktree_config; the_repository->repository_format_relative_worktrees = @@ -2042,7 +2066,8 @@ void check_repository_format(struct repository_format *fmt) repo_set_hash_algo(the_repository, fmt->hash_algo); repo_set_compat_hash_algo(the_repository, fmt->compat_hash_algo); repo_set_ref_storage_format(the_repository, - fmt->ref_storage_format); + fmt->ref_storage_format, + fmt->ref_storage_payload); the_repository->repository_format_worktree_config = fmt->worktree_config; the_repository->repository_format_relative_worktrees = @@ -2643,7 +2668,8 @@ static void repository_format_configure(struct repository_format *repo_fmt, } else { repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; } - repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format); + repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format, + repo_fmt->ref_storage_payload); } int init_db(const char *git_dir, const char *real_git_dir, diff --git a/setup.h b/setup.h index ddb9f6701c..093af39e84 100644 --- a/setup.h +++ b/setup.h @@ -171,6 +171,7 @@ struct repository_format { int hash_algo; int compat_hash_algo; enum ref_storage_format ref_storage_format; + char *ref_storage_payload; int sparse_index; char *work_tree; struct string_list unknown_extensions; diff --git a/t/meson.build b/t/meson.build index 459c52a489..11fc5a49ee 100644 --- a/t/meson.build +++ b/t/meson.build @@ -210,6 +210,7 @@ integration_tests = [ 't1420-lost-found.sh', 't1421-reflog-write.sh', 't1422-show-ref-exists.sh', + 't1423-ref-backend.sh', 't1430-bad-ref-name.sh', 't1450-fsck.sh', 't1451-fsck-buffer.sh', diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh new file mode 100755 index 0000000000..82cccb7a65 --- /dev/null +++ b/t/t1423-ref-backend.sh @@ -0,0 +1,159 @@ +#!/bin/sh + +test_description='Test reference backend URIs' + +. ./test-lib.sh + +# Run a git command with the provided reference storage. Reset the backend +# post running the command. +# Usage: run_with_uri +# is the relative path to the repo to run the command in. +# is the original ref storage of the repo. +# is the new URI to be set for the ref storage. +# is the git subcommand to be run in the repository. +run_with_uri () { + repo=$1 && + backend=$2 && + uri=$3 && + cmd=$4 && + + git -C "$repo" config set core.repositoryformatversion 1 + git -C "$repo" config set extensions.refStorage "$uri" && + git -C "$repo" $cmd && + git -C "$repo" config set extensions.refStorage "$backend" +} + +# Test a repository with a given reference storage by running and comparing +# 'git refs list' before and after setting the new reference backend. If +# err_msg is set, expect the command to fail and grep for the provided err_msg. +# Usage: run_with_uri +# is the relative path to the repo to run the command in. +# is the original ref storage of the repo. +# is the new URI to be set for the ref storage. +# (optional) if set, check if 'git-refs(1)' failed with the provided msg. +test_refs_backend () { + repo=$1 && + backend=$2 && + uri=$3 && + err_msg=$4 && + + git -C "$repo" config set core.repositoryformatversion 1 && + if test -n "$err_msg"; + then + git -C "$repo" config set extensions.refStorage "$uri" && + test_must_fail git -C "$repo" refs list 2>err && + test_grep "$err_msg" err + else + git -C "$repo" refs list >expect && + run_with_uri "$repo" "$backend" "$uri" "refs list" >actual && + test_cmp expect actual + fi +} + +test_expect_success 'URI is invalid' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "reftable@/home/reftable" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +test_expect_success 'URI ends with colon' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "reftable:" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +test_expect_success 'unknown reference backend' ' + test_when_finished "rm -rf repo" && + git init repo && + test_refs_backend repo files "db://.git" \ + "invalid value for ${SQ}extensions.refstorage${SQ}" +' + +ref_formats="files reftable" +for from_format in $ref_formats +do + +for to_format in $ref_formats +do + if test "$from_format" = "$to_format" + then + continue + fi + + + for dir in "$(pwd)/repo/.git" "." + do + + test_expect_success "read from $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" + ) + ' + + test_expect_success "write to $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" && + + git refs list >expect && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "tag -d 1" && + git refs list >actual && + test_cmp expect actual && + + git refs list | grep -v "refs/tags/1" >expect && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "refs list" >actual && + test_cmp expect actual + ) + ' + + test_expect_success "with worktree and $to_format backend, $dir dir" ' + test_when_finished "rm -rf repo wt" && + git init --ref-format=$from_format repo && + ( + cd repo && + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --dry-run --ref-format=$to_format >out && + BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && + + git config set core.repositoryformatversion 1 && + git config set extensions.refStorage "$to_format://$BACKEND_PATH" && + + git worktree add ../wt 2 + ) && + + git -C repo for-each-ref --include-root-refs >expect && + git -C wt for-each-ref --include-root-refs >expect && + ! test_cmp expect actual && + + git -C wt rev-parse 2 >expect && + git -C wt rev-parse HEAD >actual && + test_cmp expect actual + ' + done # closes dir +done # closes to_format +done # closes from_format + +test_done From 275ad58b600f929884dbf571d20f92f48b414fec Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 23 Feb 2026 09:01:41 +0100 Subject: [PATCH 6/6] refs: add GIT_REFERENCE_BACKEND to specify reference backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git allows setting a different object directory via 'GIT_OBJECT_DIRECTORY', but provides no equivalent for references. In the previous commit we extended the 'extensions.refStorage' config to also support an URI input for reference backend with location. Let's also add a new environment variable 'GIT_REFERENCE_BACKEND' that takes in the same input as the config variable. Having an environment variable allows us to modify the reference backend and location on the fly for individual Git commands. The environment variable also allows usage of alternate reference directories during 'git-clone(1)' and 'git-init(1)'. Add the config to the repository when created with the environment variable set. When initializing the repository with an alternate reference folder, create the required stubs in the repositories $GIT_DIR. The inverse, i.e. removal of the ref store doesn't clean up the stubs in the $GIT_DIR since that would render it unusable. Removal of ref store is only used when migrating between ref formats and cleanup of the $GIT_DIR doesn't make sense in such a situation. Helped-by: Jean-Noël Avila Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- Documentation/git.adoc | 5 ++ environment.h | 1 + refs.c | 30 +++++-- setup.c | 55 +++++++++++- t/t1423-ref-backend.sh | 187 +++++++++++++++++++++++++++++++++-------- 5 files changed, 233 insertions(+), 45 deletions(-) diff --git a/Documentation/git.adoc b/Documentation/git.adoc index ce099e78b8..66442735ea 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -584,6 +584,11 @@ double-quotes and respecting backslash escapes. E.g., the value repositories will be set to this value. The default is "files". See `--ref-format` in linkgit:git-init[1]. +`GIT_REFERENCE_BACKEND`:: + Specify which reference backend to be used along with its URI. + See `extensions.refStorage` option in linkgit:git-config[1] for more + details. Overrides the config variable when used. + Git Commits ~~~~~~~~~~~ `GIT_AUTHOR_NAME`:: diff --git a/environment.h b/environment.h index 27f657af04..540e0a7f6d 100644 --- a/environment.h +++ b/environment.h @@ -42,6 +42,7 @@ #define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS" #define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR" #define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE" +#define GIT_REFERENCE_BACKEND_ENVIRONMENT "GIT_REFERENCE_BACKEND" /* * Environment variable used to propagate the --no-advice global option to the diff --git a/refs.c b/refs.c index ef1902e85c..a700a66f08 100644 --- a/refs.c +++ b/refs.c @@ -2192,13 +2192,17 @@ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *e { int ret = refs->be->create_on_disk(refs, flags, err); - if (!ret && - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { - struct strbuf msg = STRBUF_INIT; - - strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); - refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); - strbuf_release(&msg); + if (!ret) { + /* Creation of stubs for linked worktrees are handled in the worktree code. */ + if (!(flags & REF_STORE_CREATE_ON_DISK_IS_WORKTREE) && refs->repo->ref_storage_payload) { + refs_create_refdir_stubs(refs->repo, refs->repo->gitdir, + "repository uses alternate refs storage"); + } else if (ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + struct strbuf msg = STRBUF_INIT; + strbuf_addf(&msg, "this repository uses the %s format", refs->be->name); + refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf); + strbuf_release(&msg); + } } return ret; @@ -2208,10 +2212,18 @@ int ref_store_remove_on_disk(struct ref_store *refs, struct strbuf *err) { int ret = refs->be->remove_on_disk(refs, err); - if (!ret && - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) { + if (!ret) { + enum ref_storage_format format = ref_storage_format_by_name(refs->be->name); struct strbuf sb = STRBUF_INIT; + /* Backends apart from the files backend create stubs. */ + if (format == REF_STORAGE_FORMAT_FILES) + return ret; + + /* Alternate refs backend require stubs in the gitdir. */ + if (refs->repo->ref_storage_payload) + return ret; + strbuf_addf(&sb, "%s/HEAD", refs->gitdir); if (unlink(sb.buf) < 0) { strbuf_addf(err, "could not delete stub HEAD: %s", diff --git a/setup.c b/setup.c index d407f3347b..90cb9be578 100644 --- a/setup.c +++ b/setup.c @@ -1838,6 +1838,7 @@ const char *setup_git_directory_gently(int *nongit_ok) static struct strbuf cwd = STRBUF_INIT; struct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT; const char *prefix = NULL; + const char *ref_backend_uri; struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; /* @@ -1995,6 +1996,25 @@ const char *setup_git_directory_gently(int *nongit_ok) setenv(GIT_PREFIX_ENVIRONMENT, "", 1); } + /* + * The env variable should override the repository config + * for 'extensions.refStorage'. + */ + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + if (ref_backend_uri) { + char *backend, *payload; + enum ref_storage_format format; + + parse_reference_uri(ref_backend_uri, &backend, &payload); + format = ref_storage_format_by_name(backend); + if (format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format: '%s'"), backend); + repo_set_ref_storage_format(the_repository, format, payload); + + free(backend); + free(payload); + } + setup_original_cwd(); strbuf_release(&dir); @@ -2337,7 +2357,8 @@ void initialize_repository_version(int hash_algo, * the remote repository's format. */ if (hash_algo != GIT_HASH_SHA1_LEGACY || - ref_storage_format != REF_STORAGE_FORMAT_FILES) + ref_storage_format != REF_STORAGE_FORMAT_FILES || + the_repository->ref_storage_payload) target_version = GIT_REPO_VERSION_READ; if (hash_algo != GIT_HASH_SHA1_LEGACY && hash_algo != GIT_HASH_UNKNOWN) @@ -2346,11 +2367,20 @@ void initialize_repository_version(int hash_algo, else if (reinit) repo_config_set_gently(the_repository, "extensions.objectformat", NULL); - if (ref_storage_format != REF_STORAGE_FORMAT_FILES) + if (the_repository->ref_storage_payload) { + struct strbuf ref_uri = STRBUF_INIT; + + strbuf_addf(&ref_uri, "%s://%s", + ref_storage_format_to_name(ref_storage_format), + the_repository->ref_storage_payload); + repo_config_set(the_repository, "extensions.refstorage", ref_uri.buf); + strbuf_release(&ref_uri); + } else if (ref_storage_format != REF_STORAGE_FORMAT_FILES) { repo_config_set(the_repository, "extensions.refstorage", ref_storage_format_to_name(ref_storage_format)); - else if (reinit) + } else if (reinit) { repo_config_set_gently(the_repository, "extensions.refstorage", NULL); + } if (reinit) { struct strbuf config = STRBUF_INIT; @@ -2623,6 +2653,7 @@ static void repository_format_configure(struct repository_format *repo_fmt, .ignore_repo = 1, .ignore_worktree = 1, }; + const char *ref_backend_uri; const char *env; config_with_options(read_default_format_config, &cfg, NULL, NULL, &opts); @@ -2668,6 +2699,24 @@ static void repository_format_configure(struct repository_format *repo_fmt, } else { repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT; } + + + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT); + if (ref_backend_uri) { + char *backend, *payload; + enum ref_storage_format format; + + parse_reference_uri(ref_backend_uri, &backend, &payload); + format = ref_storage_format_by_name(backend); + if (format == REF_STORAGE_FORMAT_UNKNOWN) + die(_("unknown ref storage format: '%s'"), backend); + + repo_fmt->ref_storage_format = format; + repo_fmt->ref_storage_payload = payload; + + free(backend); + } + repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format, repo_fmt->ref_storage_payload); } diff --git a/t/t1423-ref-backend.sh b/t/t1423-ref-backend.sh index 82cccb7a65..6061023591 100755 --- a/t/t1423-ref-backend.sh +++ b/t/t1423-ref-backend.sh @@ -11,16 +11,25 @@ test_description='Test reference backend URIs' # is the original ref storage of the repo. # is the new URI to be set for the ref storage. # is the git subcommand to be run in the repository. +# if 'config', set the backend via the 'extensions.refStorage' config. +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. run_with_uri () { repo=$1 && backend=$2 && uri=$3 && cmd=$4 && + via=$5 && - git -C "$repo" config set core.repositoryformatversion 1 - git -C "$repo" config set extensions.refStorage "$uri" && - git -C "$repo" $cmd && - git -C "$repo" config set extensions.refStorage "$backend" + git -C "$repo" config set core.repositoryformatversion 1 && + if test "$via" = "env" + then + test_env GIT_REFERENCE_BACKEND="$uri" git -C "$repo" $cmd + elif test "$via" = "config" + then + git -C "$repo" config set extensions.refStorage "$uri" && + git -C "$repo" $cmd && + git -C "$repo" config set extensions.refStorage "$backend" + fi } # Test a repository with a given reference storage by running and comparing @@ -30,44 +39,84 @@ run_with_uri () { # is the relative path to the repo to run the command in. # is the original ref storage of the repo. # is the new URI to be set for the ref storage. +# if 'config', set the backend via the 'extensions.refStorage' config. +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env. # (optional) if set, check if 'git-refs(1)' failed with the provided msg. test_refs_backend () { repo=$1 && backend=$2 && uri=$3 && - err_msg=$4 && + via=$4 && + err_msg=$5 && + - git -C "$repo" config set core.repositoryformatversion 1 && if test -n "$err_msg"; then - git -C "$repo" config set extensions.refStorage "$uri" && - test_must_fail git -C "$repo" refs list 2>err && - test_grep "$err_msg" err + if test "$via" = "env" + then + test_env GIT_REFERENCE_BACKEND="$uri" test_must_fail git -C "$repo" refs list 2>err + elif test "$via" = "config" + then + git -C "$repo" config set extensions.refStorage "$uri" && + test_must_fail git -C "$repo" refs list 2>err && + test_grep "$err_msg" err + fi else git -C "$repo" refs list >expect && - run_with_uri "$repo" "$backend" "$uri" "refs list" >actual && + run_with_uri "$repo" "$backend" "$uri" "refs list" "$via">actual && test_cmp expect actual fi } -test_expect_success 'URI is invalid' ' +# Verify that the expected files are present in the gitdir and the refsdir. +# Usage: verify_files_exist +# is the path for the gitdir. +# is the path for the refdir. +verify_files_exist () { + gitdir=$1 && + refdir=$2 && + + # verify that the stubs were added to the $GITDIR. + echo "repository uses alternate refs storage" >expect && + test_cmp expect $gitdir/refs/heads && + echo "ref: refs/heads/.invalid" >expect && + test_cmp expect $gitdir/HEAD + + # verify that backend specific files exist. + case "$GIT_DEFAULT_REF_FORMAT" in + files) + test_path_is_dir $refdir/refs/heads && + test_path_is_file $refdir/HEAD;; + reftable) + test_path_is_dir $refdir/reftable && + test_path_is_file $refdir/reftable/tables.list;; + *) + BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";; + esac +} + +methods="config env" +for method in $methods +do + +test_expect_success "$method: URI is invalid" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "reftable@/home/reftable" \ + test_refs_backend repo files "reftable@/home/reftable" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' -test_expect_success 'URI ends with colon' ' +test_expect_success "$method: URI ends with colon" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "reftable:" \ + test_refs_backend repo files "reftable:" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' -test_expect_success 'unknown reference backend' ' +test_expect_success "$method: unknown reference backend" ' test_when_finished "rm -rf repo" && git init repo && - test_refs_backend repo files "db://.git" \ + test_refs_backend repo files "db://.git" "$method" \ "invalid value for ${SQ}extensions.refstorage${SQ}" ' @@ -86,7 +135,7 @@ do for dir in "$(pwd)/repo/.git" "." do - test_expect_success "read from $to_format backend, $dir dir" ' + test_expect_success "$method: read from $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && git init --ref-format=$from_format repo && ( @@ -101,7 +150,7 @@ do ) ' - test_expect_success "write to $to_format backend, $dir dir" ' + test_expect_success "$method: write to $to_format backend, $dir dir" ' test_when_finished "rm -rf repo" && git init --ref-format=$from_format repo && ( @@ -113,20 +162,22 @@ do git refs migrate --dry-run --ref-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && - test_refs_backend . $from_format "$to_format://$BACKEND_PATH" && + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" && git refs list >expect && - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "tag -d 1" && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "tag -d 1" "$method" && git refs list >actual && test_cmp expect actual && git refs list | grep -v "refs/tags/1" >expect && - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "refs list" >actual && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "refs list" "$method" >actual && test_cmp expect actual ) ' - test_expect_success "with worktree and $to_format backend, $dir dir" ' + test_expect_success "$method: with worktree and $to_format backend, $dir dir" ' test_when_finished "rm -rf repo wt" && git init --ref-format=$from_format repo && ( @@ -138,22 +189,92 @@ do git refs migrate --dry-run --ref-format=$to_format >out && BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" && - git config set core.repositoryformatversion 1 && - git config set extensions.refStorage "$to_format://$BACKEND_PATH" && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "worktree add ../wt 2" "$method" && - git worktree add ../wt 2 - ) && + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "for-each-ref --include-root-refs" "$method" >actual && + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \ + "for-each-ref --include-root-refs" "$method" >expect && + ! test_cmp expect actual && - git -C repo for-each-ref --include-root-refs >expect && - git -C wt for-each-ref --include-root-refs >expect && - ! test_cmp expect actual && - - git -C wt rev-parse 2 >expect && - git -C wt rev-parse HEAD >actual && - test_cmp expect actual + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \ + "rev-parse 2" "$method" >actual && + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \ + "rev-parse HEAD" "$method" >expect && + test_cmp expect actual + ) ' done # closes dir + + test_expect_success "migrating repository to $to_format with alternate refs directory" ' + test_when_finished "rm -rf repo refdir" && + mkdir refdir && + GIT_REFERENCE_BACKEND="${from_format}://$(pwd)/refdir" git init repo && + ( + cd repo && + + test_commit 1 && + test_commit 2 && + test_commit 3 && + + git refs migrate --ref-format=$to_format && + git refs list >out && + test_grep "refs/tags/1" out && + test_grep "refs/tags/2" out && + test_grep "refs/tags/3" out + ) + ' + done # closes to_format done # closes from_format +done # closes method + +test_expect_success 'initializing repository with alt ref directory' ' + test_when_finished "rm -rf repo refdir" && + mkdir refdir && + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && + GIT_REFERENCE_BACKEND=$BACKEND git init repo && + verify_files_exist repo/.git refdir && + ( + cd repo && + + git config get extensions.refstorage >expect && + echo $BACKEND >actual && + test_cmp expect actual && + + test_commit 1 && + test_commit 2 && + test_commit 3 && + git refs list >out && + test_grep "refs/tags/1" out && + test_grep "refs/tags/2" out && + test_grep "refs/tags/3" out + ) +' + +test_expect_success 'cloning repository with alt ref directory' ' + test_when_finished "rm -rf source repo refdir" && + mkdir refdir && + + git init source && + test_commit -C source 1 && + test_commit -C source 2 && + test_commit -C source 3 && + + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" && + GIT_REFERENCE_BACKEND=$BACKEND git clone source repo && + + git -C repo config get extensions.refstorage >expect && + echo $BACKEND >actual && + test_cmp expect actual && + + verify_files_exist repo/.git refdir && + + git -C source for-each-ref refs/tags/ >expect && + git -C repo for-each-ref refs/tags/ >actual && + test_cmp expect actual +' + test_done