aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-03-04 10:52:58 -0800
committerJunio C Hamano <gitster@pobox.com>2026-03-04 10:52:59 -0800
commit1d0a2acb78f157d39937a088548e561b27722e8d (patch)
tree4251f1f0f2a4ce4a6ee6f71e75d17d0bdbe4f948
parent50d063e335afd5828fbb9de2f2b2fb44fd884d2b (diff)
parent53592d68e86814fcc4a8df6cc38340597e56fe5a (diff)
downloadgit-1d0a2acb78f157d39937a088548e561b27722e8d.tar.xz
Merge branch 'kn/ref-location'
Allow the directory in which reference backends store their data to be specified. * kn/ref-location: refs: add GIT_REFERENCE_BACKEND to specify reference backend refs: allow reference location in refstorage config refs: receive and use the reference storage payload refs: move out stub modification to generic layer refs: extract out `refs_create_refdir_stubs()` setup: don't modify repo in `create_reference_database()`
-rw-r--r--Documentation/config/extensions.adoc16
-rw-r--r--Documentation/git.adoc5
-rw-r--r--builtin/clone.c9
-rw-r--r--builtin/worktree.c34
-rw-r--r--environment.h1
-rw-r--r--refs.c126
-rw-r--r--refs.h13
-rw-r--r--refs/files-backend.c23
-rw-r--r--refs/packed-backend.c5
-rw-r--r--refs/packed-backend.h1
-rw-r--r--refs/refs-internal.h14
-rw-r--r--refs/reftable-backend.c61
-rw-r--r--repository.c9
-rw-r--r--repository.h8
-rw-r--r--setup.c96
-rw-r--r--setup.h4
-rw-r--r--t/meson.build1
-rwxr-xr-xt/t1423-ref-backend.sh280
18 files changed, 625 insertions, 81 deletions
diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc
index 2aef3315b1..be6678bb5b 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 `<format>://<payload>` 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/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/builtin/clone.c b/builtin/clone.c
index 45d8fa0eed..70c8a68e6f 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -1228,12 +1228,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
@@ -1445,7 +1440,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/builtin/worktree.c b/builtin/worktree.c
index 3d6547c23b..38d6fd9027 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/<wt_id>'.
+ * But when using alternate reference directories, we want to store the worktree
+ * references in '$ALTERNATE_REFERENCE_DIR/worktrees/<wt_id>'.
+ *
+ * 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/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 600913b99f..0ad1da990d 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"
@@ -2166,15 +2167,93 @@ 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)
{
- return refs->be->create_on_disk(refs, flags, err);
+ int ret = refs->be->create_on_disk(refs, flags, err);
+
+ 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;
}
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) {
+ 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",
+ 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,
@@ -2227,7 +2306,11 @@ static struct ref_store *ref_store_init(struct repository *repo,
if (!be)
BUG("reference backend is unknown");
- refs = be->init(repo, 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;
}
@@ -3410,3 +3493,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.h b/refs.h
index f16b1b697b..f3a1d604ad 100644
--- a/refs.h
+++ b/refs.h
@@ -1428,4 +1428,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_content', 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_content);
+
#endif /* REFS_H */
diff --git a/refs/files-backend.c b/refs/files-backend.c
index b1b13b41f6..d2c4be1653 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;
+
+ refs_compute_filesystem_location(gitdir, payload, &is_worktree, &refdir,
+ &ref_common_dir);
- base_ref_store_init(ref_store, repo, gitdir, &refs_be_files);
+ 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;
}
@@ -3699,7 +3706,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/packed-backend.c b/refs/packed-backend.c
index 59b3ecb9d6..8872a7fe18 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 191a25683f..d79e35fd26 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -391,6 +391,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);
/*
@@ -668,4 +669,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 5611808ad7..57b5d48905 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;
}
@@ -491,19 +495,6 @@ 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);
-
strbuf_release(&sb);
return 0;
}
@@ -529,30 +520,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;
diff --git a/repository.c b/repository.c
index 46a7c99930..44e77cd05a 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;
@@ -370,6 +374,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 7141237f97..72a5e9d410 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;
@@ -205,7 +210,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 c8336eb20e..393b970ae4 100644
--- a/setup.c
+++ b/setup.c
@@ -631,6 +631,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.
*/
@@ -673,10 +688,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);
@@ -852,6 +874,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);
}
@@ -1817,6 +1840,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;
/*
@@ -1944,7 +1968,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 =
@@ -1975,6 +2000,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);
@@ -2046,7 +2090,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_submodule_path_cfg =
@@ -2319,7 +2364,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)
@@ -2328,11 +2374,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;
@@ -2375,14 +2430,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);
@@ -2616,6 +2669,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);
@@ -2661,7 +2715,26 @@ 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);
+
+
+ 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);
}
int init_db(const char *git_dir, const char *real_git_dir,
@@ -2717,8 +2790,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 0738dec244..bcb16b0b4a 100644
--- a/setup.h
+++ b/setup.h
@@ -172,6 +172,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;
@@ -241,8 +242,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!!
diff --git a/t/meson.build b/t/meson.build
index 6d91470ebc..505cda2988 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -213,6 +213,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..fd47d77e8e
--- /dev/null
+++ b/t/t1423-ref-backend.sh
@@ -0,0 +1,280 @@
+#!/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 <repo> <backend> <uri> <cmd>
+# <repo> is the relative path to the repo to run the command in.
+# <backend> is the original ref storage of the repo.
+# <uri> is the new URI to be set for the ref storage.
+# <cmd> is the git subcommand to be run in the repository.
+# <via> 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 &&
+ 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
+# '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 <repo> <backend> <uri> <cmd>
+# <repo> is the relative path to the repo to run the command in.
+# <backend> is the original ref storage of the repo.
+# <uri> is the new URI to be set for the ref storage.
+# <via> if 'config', set the backend via the 'extensions.refStorage' config.
+# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env.
+# <err_msg> (optional) if set, check if 'git-refs(1)' failed with the provided msg.
+test_refs_backend () {
+ repo=$1 &&
+ backend=$2 &&
+ uri=$3 &&
+ via=$4 &&
+ err_msg=$5 &&
+
+
+ if test -n "$err_msg";
+ then
+ 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" "$via">actual &&
+ test_cmp expect actual
+ fi
+}
+
+# Verify that the expected files are present in the gitdir and the refsdir.
+# Usage: verify_files_exist <gitdir> <refdir>
+# <gitdir> is the path for the gitdir.
+# <refdir> 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" "$method" \
+ "invalid value for ${SQ}extensions.refstorage${SQ}"
+'
+
+test_expect_success "$method: URI ends with colon" '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ test_refs_backend repo files "reftable:" "$method" \
+ "invalid value for ${SQ}extensions.refstorage${SQ}"
+'
+
+test_expect_success "$method: unknown reference backend" '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ test_refs_backend repo files "db://.git" "$method" \
+ "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 "$method: 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 "$method: 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" "$method" &&
+
+ git refs list >expect &&
+ 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" "$method" >actual &&
+ test_cmp expect actual
+ )
+ '
+
+ 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 &&
+ (
+ 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)" &&
+
+ run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
+ "worktree add ../wt 2" "$method" &&
+
+ 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 &&
+
+ 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 >actual &&
+ echo $BACKEND >expect &&
+ 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 >actual &&
+ echo $BACKEND >expect &&
+ 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