refs: add GIT_REFERENCE_BACKEND to specify reference backend

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 <jn.avila@free.fr> Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Feb 25, 2026 at 10:40 UTC 53592d68e86814fcc4a8df6cc38340597e56fe5a
5 files changed +233 -45
Documentation/git.adoc
+5
@@ -584,6 +584,11 @@ double-quotes and respecting backslash escapes. E.g., the value
584 repositories will be set to this value. The default is "files".
585 See `--ref-format` in linkgit:git-init[1].
586
587 +`GIT_REFERENCE_BACKEND`::
588 + Specify which reference backend to be used along with its URI.
589 + See `extensions.refStorage` option in linkgit:git-config[1] for more
590 + details. Overrides the config variable when used.
591 +
592 Git Commits
593 ~~~~~~~~~~~
594 `GIT_AUTHOR_NAME`::
environment.h
+1
@@ -42,6 +42,7 @@
42 #define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS"
43 #define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR"
44 #define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE"
45 +#define GIT_REFERENCE_BACKEND_ENVIRONMENT "GIT_REFERENCE_BACKEND"
46
47 /*
48 * Environment variable used to propagate the --no-advice global option to the
refs.c
+21 -9
@@ -2192,13 +2192,17 @@ int ref_store_create_on_disk(struct ref_store *refs, int flags, struct strbuf *e
2192 {
2193 int ret = refs->be->create_on_disk(refs, flags, err);
2194
2195 - if (!ret &&
2196 - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) {
2197 - struct strbuf msg = STRBUF_INIT;
2198 -
2199 - strbuf_addf(&msg, "this repository uses the %s format", refs->be->name);
2200 - refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf);
2201 - strbuf_release(&msg);
2195 + if (!ret) {
2196 + /* Creation of stubs for linked worktrees are handled in the worktree code. */
2197 + if (!(flags & REF_STORE_CREATE_ON_DISK_IS_WORKTREE) && refs->repo->ref_storage_payload) {
2198 + refs_create_refdir_stubs(refs->repo, refs->repo->gitdir,
2199 + "repository uses alternate refs storage");
2200 + } else if (ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) {
2201 + struct strbuf msg = STRBUF_INIT;
2202 + strbuf_addf(&msg, "this repository uses the %s format", refs->be->name);
2203 + refs_create_refdir_stubs(refs->repo, refs->gitdir, msg.buf);
2204 + strbuf_release(&msg);
2205 + }
2206 }
2207
2208 return ret;
@@ -2208,10 +2212,18 @@ int ref_store_remove_on_disk(struct ref_store *refs, struct strbuf *err)
2212 {
2213 int ret = refs->be->remove_on_disk(refs, err);
2214
2211 - if (!ret &&
2212 - ref_storage_format_by_name(refs->be->name) != REF_STORAGE_FORMAT_FILES) {
2215 + if (!ret) {
2216 + enum ref_storage_format format = ref_storage_format_by_name(refs->be->name);
2217 struct strbuf sb = STRBUF_INIT;
2218
2219 + /* Backends apart from the files backend create stubs. */
2220 + if (format == REF_STORAGE_FORMAT_FILES)
2221 + return ret;
2222 +
2223 + /* Alternate refs backend require stubs in the gitdir. */
2224 + if (refs->repo->ref_storage_payload)
2225 + return ret;
2226 +
2227 strbuf_addf(&sb, "%s/HEAD", refs->gitdir);
2228 if (unlink(sb.buf) < 0) {
2229 strbuf_addf(err, "could not delete stub HEAD: %s",
setup.c
+52 -3
@@ -1838,6 +1838,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
1838 static struct strbuf cwd = STRBUF_INIT;
1839 struct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT, report = STRBUF_INIT;
1840 const char *prefix = NULL;
1841 + const char *ref_backend_uri;
1842 struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
1843
1844 /*
@@ -1995,6 +1996,25 @@ const char *setup_git_directory_gently(int *nongit_ok)
1996 setenv(GIT_PREFIX_ENVIRONMENT, "", 1);
1997 }
1998
1999 + /*
2000 + * The env variable should override the repository config
2001 + * for 'extensions.refStorage'.
2002 + */
2003 + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);
2004 + if (ref_backend_uri) {
2005 + char *backend, *payload;
2006 + enum ref_storage_format format;
2007 +
2008 + parse_reference_uri(ref_backend_uri, &backend, &payload);
2009 + format = ref_storage_format_by_name(backend);
2010 + if (format == REF_STORAGE_FORMAT_UNKNOWN)
2011 + die(_("unknown ref storage format: '%s'"), backend);
2012 + repo_set_ref_storage_format(the_repository, format, payload);
2013 +
2014 + free(backend);
2015 + free(payload);
2016 + }
2017 +
2018 setup_original_cwd();
2019
2020 strbuf_release(&dir);
@@ -2337,7 +2357,8 @@ void initialize_repository_version(int hash_algo,
2357 * the remote repository's format.
2358 */
2359 if (hash_algo != GIT_HASH_SHA1_LEGACY ||
2340 - ref_storage_format != REF_STORAGE_FORMAT_FILES)
2360 + ref_storage_format != REF_STORAGE_FORMAT_FILES ||
2361 + the_repository->ref_storage_payload)
2362 target_version = GIT_REPO_VERSION_READ;
2363
2364 if (hash_algo != GIT_HASH_SHA1_LEGACY && hash_algo != GIT_HASH_UNKNOWN)
@@ -2346,11 +2367,20 @@ void initialize_repository_version(int hash_algo,
2367 else if (reinit)
2368 repo_config_set_gently(the_repository, "extensions.objectformat", NULL);
2369
2349 - if (ref_storage_format != REF_STORAGE_FORMAT_FILES)
2370 + if (the_repository->ref_storage_payload) {
2371 + struct strbuf ref_uri = STRBUF_INIT;
2372 +
2373 + strbuf_addf(&ref_uri, "%s://%s",
2374 + ref_storage_format_to_name(ref_storage_format),
2375 + the_repository->ref_storage_payload);
2376 + repo_config_set(the_repository, "extensions.refstorage", ref_uri.buf);
2377 + strbuf_release(&ref_uri);
2378 + } else if (ref_storage_format != REF_STORAGE_FORMAT_FILES) {
2379 repo_config_set(the_repository, "extensions.refstorage",
2380 ref_storage_format_to_name(ref_storage_format));
2352 - else if (reinit)
2381 + } else if (reinit) {
2382 repo_config_set_gently(the_repository, "extensions.refstorage", NULL);
2383 + }
2384
2385 if (reinit) {
2386 struct strbuf config = STRBUF_INIT;
@@ -2623,6 +2653,7 @@ static void repository_format_configure(struct repository_format *repo_fmt,
2653 .ignore_repo = 1,
2654 .ignore_worktree = 1,
2655 };
2656 + const char *ref_backend_uri;
2657 const char *env;
2658
2659 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,
2699 } else {
2700 repo_fmt->ref_storage_format = REF_STORAGE_FORMAT_DEFAULT;
2701 }
2702 +
2703 +
2704 + ref_backend_uri = getenv(GIT_REFERENCE_BACKEND_ENVIRONMENT);
2705 + if (ref_backend_uri) {
2706 + char *backend, *payload;
2707 + enum ref_storage_format format;
2708 +
2709 + parse_reference_uri(ref_backend_uri, &backend, &payload);
2710 + format = ref_storage_format_by_name(backend);
2711 + if (format == REF_STORAGE_FORMAT_UNKNOWN)
2712 + die(_("unknown ref storage format: '%s'"), backend);
2713 +
2714 + repo_fmt->ref_storage_format = format;
2715 + repo_fmt->ref_storage_payload = payload;
2716 +
2717 + free(backend);
2718 + }
2719 +
2720 repo_set_ref_storage_format(the_repository, repo_fmt->ref_storage_format,
2721 repo_fmt->ref_storage_payload);
2722 }
t/t1423-ref-backend.sh
+154 -33
@@ -11,16 +11,25 @@ test_description='Test reference backend URIs'
11 # <backend> is the original ref storage of the repo.
12 # <uri> is the new URI to be set for the ref storage.
13 # <cmd> is the git subcommand to be run in the repository.
14 +# <via> if 'config', set the backend via the 'extensions.refStorage' config.
15 +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env.
16 run_with_uri () {
17 repo=$1 &&
18 backend=$2 &&
19 uri=$3 &&
20 cmd=$4 &&
21 + via=$5 &&
22
20 - git -C "$repo" config set core.repositoryformatversion 1
21 - git -C "$repo" config set extensions.refStorage "$uri" &&
22 - git -C "$repo" $cmd &&
23 - git -C "$repo" config set extensions.refStorage "$backend"
23 + git -C "$repo" config set core.repositoryformatversion 1 &&
24 + if test "$via" = "env"
25 + then
26 + test_env GIT_REFERENCE_BACKEND="$uri" git -C "$repo" $cmd
27 + elif test "$via" = "config"
28 + then
29 + git -C "$repo" config set extensions.refStorage "$uri" &&
30 + git -C "$repo" $cmd &&
31 + git -C "$repo" config set extensions.refStorage "$backend"
32 + fi
33 }
34
35 # Test a repository with a given reference storage by running and comparing
@@ -30,44 +39,84 @@ run_with_uri () {
39 # <repo> is the relative path to the repo to run the command in.
40 # <backend> is the original ref storage of the repo.
41 # <uri> is the new URI to be set for the ref storage.
42 +# <via> if 'config', set the backend via the 'extensions.refStorage' config.
43 +# if 'env', set the backend via the 'GIT_REFERENCE_BACKEND' env.
44 # <err_msg> (optional) if set, check if 'git-refs(1)' failed with the provided msg.
45 test_refs_backend () {
46 repo=$1 &&
47 backend=$2 &&
48 uri=$3 &&
38 - err_msg=$4 &&
49 + via=$4 &&
50 + err_msg=$5 &&
51 +
52
40 - git -C "$repo" config set core.repositoryformatversion 1 &&
53 if test -n "$err_msg";
54 then
43 - git -C "$repo" config set extensions.refStorage "$uri" &&
44 - test_must_fail git -C "$repo" refs list 2>err &&
45 - test_grep "$err_msg" err
55 + if test "$via" = "env"
56 + then
57 + test_env GIT_REFERENCE_BACKEND="$uri" test_must_fail git -C "$repo" refs list 2>err
58 + elif test "$via" = "config"
59 + then
60 + git -C "$repo" config set extensions.refStorage "$uri" &&
61 + test_must_fail git -C "$repo" refs list 2>err &&
62 + test_grep "$err_msg" err
63 + fi
64 else
65 git -C "$repo" refs list >expect &&
48 - run_with_uri "$repo" "$backend" "$uri" "refs list" >actual &&
66 + run_with_uri "$repo" "$backend" "$uri" "refs list" "$via">actual &&
67 test_cmp expect actual
68 fi
69 }
70
53 -test_expect_success 'URI is invalid' '
71 +# Verify that the expected files are present in the gitdir and the refsdir.
72 +# Usage: verify_files_exist <gitdir> <refdir>
73 +# <gitdir> is the path for the gitdir.
74 +# <refdir> is the path for the refdir.
75 +verify_files_exist () {
76 + gitdir=$1 &&
77 + refdir=$2 &&
78 +
79 + # verify that the stubs were added to the $GITDIR.
80 + echo "repository uses alternate refs storage" >expect &&
81 + test_cmp expect $gitdir/refs/heads &&
82 + echo "ref: refs/heads/.invalid" >expect &&
83 + test_cmp expect $gitdir/HEAD
84 +
85 + # verify that backend specific files exist.
86 + case "$GIT_DEFAULT_REF_FORMAT" in
87 + files)
88 + test_path_is_dir $refdir/refs/heads &&
89 + test_path_is_file $refdir/HEAD;;
90 + reftable)
91 + test_path_is_dir $refdir/reftable &&
92 + test_path_is_file $refdir/reftable/tables.list;;
93 + *)
94 + BUG "unhandled ref format $GIT_DEFAULT_REF_FORMAT";;
95 + esac
96 +}
97 +
98 +methods="config env"
99 +for method in $methods
100 +do
101 +
102 +test_expect_success "$method: URI is invalid" '
103 test_when_finished "rm -rf repo" &&
104 git init repo &&
56 - test_refs_backend repo files "reftable@/home/reftable" \
105 + test_refs_backend repo files "reftable@/home/reftable" "$method" \
106 "invalid value for ${SQ}extensions.refstorage${SQ}"
107 '
108
60 -test_expect_success 'URI ends with colon' '
109 +test_expect_success "$method: URI ends with colon" '
110 test_when_finished "rm -rf repo" &&
111 git init repo &&
63 - test_refs_backend repo files "reftable:" \
112 + test_refs_backend repo files "reftable:" "$method" \
113 "invalid value for ${SQ}extensions.refstorage${SQ}"
114 '
115
67 -test_expect_success 'unknown reference backend' '
116 +test_expect_success "$method: unknown reference backend" '
117 test_when_finished "rm -rf repo" &&
118 git init repo &&
70 - test_refs_backend repo files "db://.git" \
119 + test_refs_backend repo files "db://.git" "$method" \
120 "invalid value for ${SQ}extensions.refstorage${SQ}"
121 '
122
@@ -86,7 +135,7 @@ do
135 for dir in "$(pwd)/repo/.git" "."
136 do
137
89 - test_expect_success "read from $to_format backend, $dir dir" '
138 + test_expect_success "$method: read from $to_format backend, $dir dir" '
139 test_when_finished "rm -rf repo" &&
140 git init --ref-format=$from_format repo &&
141 (
@@ -101,7 +150,7 @@ do
150 )
151 '
152
104 - test_expect_success "write to $to_format backend, $dir dir" '
153 + test_expect_success "$method: write to $to_format backend, $dir dir" '
154 test_when_finished "rm -rf repo" &&
155 git init --ref-format=$from_format repo &&
156 (
@@ -113,20 +162,22 @@ do
162 git refs migrate --dry-run --ref-format=$to_format >out &&
163 BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" &&
164
116 - test_refs_backend . $from_format "$to_format://$BACKEND_PATH" &&
165 + test_refs_backend . $from_format "$to_format://$BACKEND_PATH" "$method" &&
166
167 git refs list >expect &&
119 - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "tag -d 1" &&
168 + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
169 + "tag -d 1" "$method" &&
170 git refs list >actual &&
171 test_cmp expect actual &&
172
173 git refs list | grep -v "refs/tags/1" >expect &&
124 - run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" "refs list" >actual &&
174 + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
175 + "refs list" "$method" >actual &&
176 test_cmp expect actual
177 )
178 '
179
129 - test_expect_success "with worktree and $to_format backend, $dir dir" '
180 + test_expect_success "$method: with worktree and $to_format backend, $dir dir" '
181 test_when_finished "rm -rf repo wt" &&
182 git init --ref-format=$from_format repo &&
183 (
@@ -138,22 +189,92 @@ do
189 git refs migrate --dry-run --ref-format=$to_format >out &&
190 BACKEND_PATH="$dir/$(sed "s/.* ${SQ}.git\/\(.*\)${SQ}/\1/" out)" &&
191
141 - git config set core.repositoryformatversion 1 &&
142 - git config set extensions.refStorage "$to_format://$BACKEND_PATH" &&
143 -
144 - git worktree add ../wt 2
145 - ) &&
192 + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
193 + "worktree add ../wt 2" "$method" &&
194
147 - git -C repo for-each-ref --include-root-refs >expect &&
148 - git -C wt for-each-ref --include-root-refs >expect &&
149 - ! test_cmp expect actual &&
195 + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
196 + "for-each-ref --include-root-refs" "$method" >actual &&
197 + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \
198 + "for-each-ref --include-root-refs" "$method" >expect &&
199 + ! test_cmp expect actual &&
200
151 - git -C wt rev-parse 2 >expect &&
152 - git -C wt rev-parse HEAD >actual &&
153 - test_cmp expect actual
201 + run_with_uri . "$from_format" "$to_format://$BACKEND_PATH" \
202 + "rev-parse 2" "$method" >actual &&
203 + run_with_uri ../wt "$from_format" "$to_format://$BACKEND_PATH" \
204 + "rev-parse HEAD" "$method" >expect &&
205 + test_cmp expect actual
206 + )
207 '
208 done # closes dir
209 +
210 + test_expect_success "migrating repository to $to_format with alternate refs directory" '
211 + test_when_finished "rm -rf repo refdir" &&
212 + mkdir refdir &&
213 + GIT_REFERENCE_BACKEND="${from_format}://$(pwd)/refdir" git init repo &&
214 + (
215 + cd repo &&
216 +
217 + test_commit 1 &&
218 + test_commit 2 &&
219 + test_commit 3 &&
220 +
221 + git refs migrate --ref-format=$to_format &&
222 + git refs list >out &&
223 + test_grep "refs/tags/1" out &&
224 + test_grep "refs/tags/2" out &&
225 + test_grep "refs/tags/3" out
226 + )
227 + '
228 +
229 done # closes to_format
230 done # closes from_format
231
232 +done # closes method
233 +
234 +test_expect_success 'initializing repository with alt ref directory' '
235 + test_when_finished "rm -rf repo refdir" &&
236 + mkdir refdir &&
237 + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" &&
238 + GIT_REFERENCE_BACKEND=$BACKEND git init repo &&
239 + verify_files_exist repo/.git refdir &&
240 + (
241 + cd repo &&
242 +
243 + git config get extensions.refstorage >actual &&
244 + echo $BACKEND >expect &&
245 + test_cmp expect actual &&
246 +
247 + test_commit 1 &&
248 + test_commit 2 &&
249 + test_commit 3 &&
250 + git refs list >out &&
251 + test_grep "refs/tags/1" out &&
252 + test_grep "refs/tags/2" out &&
253 + test_grep "refs/tags/3" out
254 + )
255 +'
256 +
257 +test_expect_success 'cloning repository with alt ref directory' '
258 + test_when_finished "rm -rf source repo refdir" &&
259 + mkdir refdir &&
260 +
261 + git init source &&
262 + test_commit -C source 1 &&
263 + test_commit -C source 2 &&
264 + test_commit -C source 3 &&
265 +
266 + BACKEND="$(test_detect_ref_format)://$(pwd)/refdir" &&
267 + GIT_REFERENCE_BACKEND=$BACKEND git clone source repo &&
268 +
269 + git -C repo config get extensions.refstorage >actual &&
270 + echo $BACKEND >expect &&
271 + test_cmp expect actual &&
272 +
273 + verify_files_exist repo/.git refdir &&
274 +
275 + git -C source for-each-ref refs/tags/ >expect &&
276 + git -C repo for-each-ref refs/tags/ >actual &&
277 + test_cmp expect actual
278 +'
279 +
280 test_done