Merge branch 'ps/odb-alternates-at-creation' into jch

The setup of alternates has been deferred to object database
creation time during clone, which drops the unused ad-hoc alternate
writing API, simplifying the object database backend interface.

* ps/odb-alternates-at-creation:
  odb/source: remove the ability to write alternates
  builtin/clone: write alternates via `odb_create_on_disk()`
  odb/source: support writing alternates when creating the database
  builtin/clone: move setup of alternates for non-shared local clones
  builtin/clone: move setup of alternates for shared local clones
  builtin/clone: refactor handling of "--reference{,-if-able}"
  builtin/clone: move around `setup_reference()`
  builtin/clone: defer setup of the object database
jch
Junio C Hamano 2026-08-31 11:12:02 -07:00
commit 7dfd47023a
11 changed files with 203 additions and 170 deletions

View File

@ -154,24 +154,26 @@ static char *get_repo_path(const char *repo, int *is_bundle)
return canon;
}

static int add_one_reference(struct string_list_item *item, void *cb_data)
struct add_one_alternate_data {
struct strvec *alternates;
int required;
};

static int add_one_alternate(struct string_list_item *item, void *cb_data)
{
struct add_one_alternate_data *data = cb_data;
struct strbuf err = STRBUF_INIT;
int *required = cb_data;
char *ref_git = compute_alternate_path(item->string, &err);

if (!ref_git) {
if (*required)
if (data->required)
die("%s", err.buf);
else
fprintf(stderr,
_("info: Could not add alternate for '%s': %s\n"),
item->string, err.buf);
} else {
struct strbuf sb = STRBUF_INIT;
strbuf_addf(&sb, "%s/objects", ref_git);
odb_add_to_alternates_file(the_repository->objects, sb.buf);
strbuf_release(&sb);
strvec_pushf(data->alternates, "%s/objects", ref_git);
}

strbuf_release(&err);
@ -179,17 +181,7 @@ static int add_one_reference(struct string_list_item *item, void *cb_data)
return 0;
}

static void setup_reference(void)
{
int required = 1;
for_each_string_list(&option_required_reference,
add_one_reference, &required);
required = 0;
for_each_string_list(&option_optional_reference,
add_one_reference, &required);
}

static void copy_alternates(struct strbuf *src, const char *src_repo)
static void read_alternates(struct strvec *alternates, const char *src_repo)
{
/*
* Read from the source objects/info/alternates file
@ -203,29 +195,70 @@ static void copy_alternates(struct strbuf *src, const char *src_repo)
* to turn entries with paths relative to the original
* absolute, so that they can be used in the new repository.
*/
FILE *in = xfopen(src->buf, "r");
FILE *in;
struct strbuf path = STRBUF_INIT;
struct strbuf line = STRBUF_INIT;

strbuf_addf(&path, "%s/objects/info/alternates", src_repo);

in = fopen(path.buf, "r");
if (!in) {
if (errno == ENOENT)
goto out;
die_errno("could not read alternates file '%s'", path.buf);
}

while (strbuf_getline(&line, in) != EOF) {
char *abs_path;
if (!line.len || line.buf[0] == '#')
continue;
if (is_absolute_path(line.buf)) {
odb_add_to_alternates_file(the_repository->objects,
line.buf);
strvec_push(alternates, line.buf);
continue;
}
abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf);
if (!normalize_path_copy(abs_path, abs_path))
odb_add_to_alternates_file(the_repository->objects,
abs_path);
strvec_push(alternates, abs_path);
else
warning("skipping invalid relative alternate: %s/%s",
src_repo, line.buf);
free(abs_path);
}

out:
strbuf_release(&path);
strbuf_release(&line);
fclose(in);
if (in)
fclose(in);
}

static void collect_alternates(struct strvec *alternates,
const char *src_repo, bool is_local)
{
if (option_required_reference.nr || option_optional_reference.nr) {
struct add_one_alternate_data data = {
.alternates = alternates,
.required = 1,
};

for_each_string_list(&option_required_reference,
add_one_alternate, &data);
data.required = 0;
for_each_string_list(&option_optional_reference,
add_one_alternate, &data);
}

if (is_local) {
struct strbuf commondir = STRBUF_INIT;

get_common_dir(&commondir, src_repo);
if (option_shared)
strvec_pushf(alternates, "%s/objects", commondir.buf);
else
read_alternates(alternates, commondir.buf);

strbuf_release(&commondir);
}
}

static void mkdir_if_missing(const char *pathname, mode_t mode)
@ -301,11 +334,9 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
continue;
}

/* Files that cannot be copied bit-for-bit... */
if (!fspathcmp(iter->relative_path, "info/alternates")) {
copy_alternates(src, src_repo);
/* Alternates were already handled earlier. */
if (!fspathcmp(iter->relative_path, "info/alternates"))
continue;
}

if (unlink(dest->buf) && errno != ENOENT)
die_errno(_("failed to unlink '%s'"), dest->buf);
@ -349,13 +380,7 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,

static void clone_local(const char *src_repo, const char *dest_repo)
{
if (option_shared) {
struct strbuf alt = STRBUF_INIT;
get_common_dir(&alt, src_repo);
strbuf_addstr(&alt, "/objects");
odb_add_to_alternates_file(the_repository->objects, alt.buf);
strbuf_release(&alt);
} else {
if (!option_shared) {
struct strbuf src = STRBUF_INIT;
struct strbuf dest = STRBUF_INIT;
get_common_dir(&src, src_repo);
@ -999,6 +1024,7 @@ int cmd_clone(int argc,
N_("uri"), N_("a URI for downloading bundles before fetching from origin remote")),
OPT_END()
};
struct strvec alternates = STRVEC_INIT;

const char * const builtin_clone_usage[] = {
N_("git clone [<options>] [--] <repo> [<dir>]"),
@ -1184,11 +1210,14 @@ int cmd_clone(int argc,
* database. We do not yet know about the object format of the
* repository, and reference backends may persist that information into
* their on-disk data structures.
*
* Furthermore, we skip initializing the object database so that we can
* first resolve potential alternates before creating it.
*/
init_db(the_repository, git_dir, real_git_dir, work_tree, option_template,
GIT_HASH_UNKNOWN, ref_storage_format, NULL,
do_not_override_repo_unix_permissions,
INIT_DB_QUIET | INIT_DB_SKIP_REFDB);
INIT_DB_QUIET | INIT_DB_SKIP_REFDB | INIT_DB_SKIP_ODB);

if (real_git_dir) {
free((char *)git_dir);
@ -1311,9 +1340,6 @@ int cmd_clone(int argc,
strbuf_reset(&key);
}

if (option_required_reference.nr || option_optional_reference.nr)
setup_reference();

remote = remote_get_early(remote_name);

if (!option_rev)
@ -1342,6 +1368,9 @@ int cmd_clone(int argc,
if (option_local > 0 && !is_local)
warning(_("--local is ignored"));

collect_alternates(&alternates, path, is_local);
create_object_database(the_repository, &alternates);

transport = transport_get(remote, path ? path : remote->url.v[0]);
transport_set_verbosity(transport, option_verbosity, option_progress);
transport->family = family;
@ -1637,6 +1666,7 @@ int cmd_clone(int argc,
string_list_clear(&option_not, 0);
string_list_clear(&option_config, 0);
string_list_clear(&server_options, 0);
strvec_clear(&alternates);

free(remote_name);
strbuf_release(&reflog_msg);

9
odb.c
View File

@ -238,15 +238,6 @@ static struct odb_source *odb_add_alternate_recursively(struct object_database *
return alternate;
}

void odb_add_to_alternates_file(struct object_database *odb,
const char *dir)
{
int ret = odb_source_write_alternate(odb->sources, dir);
if (ret < 0)
die(NULL);
odb_add_alternate_recursively(odb, dir, 0);
}

struct odb_source *odb_add_to_alternates_memory(struct object_database *odb,
const char *dir)
{

7
odb.h
View File

@ -265,13 +265,6 @@ int odb_mkstemp(struct object_database *odb,
*/
int odb_has_alternates(struct object_database *odb);

/*
* Add the directory to the on-disk alternates file; the new entry will also
* take effect in the current process.
*/
void odb_add_to_alternates_file(struct object_database *odb,
const char *dir);

/*
* Add the directory to the in-memory list of alternate sources (along with any
* recursive alternates it points to), but do not modify the on-disk alternates

View File

@ -19,6 +19,7 @@
#include "run-command.h"
#include "strbuf.h"
#include "string-list.h"
#include "strmap.h"
#include "strvec.h"
#include "tree.h"
#include "write-or-die.h"
@ -51,9 +52,14 @@ static void odb_source_files_close(struct odb_source *source)
odb_source_close(&files->packed->base);
}

static int odb_source_files_create_on_disk(struct odb_source *source)
static int odb_source_files_create_on_disk(struct odb_source *source,
const struct odb_create_on_disk_options *opts)
{
struct lock_file alternates_lock = LOCK_INIT;
struct strbuf path = STRBUF_INIT;
struct strset seen = STRSET_INIT;
struct strbuf line = STRBUF_INIT;
int ret;

safe_create_dir(source->odb->repo, source->path, 1);

@ -64,8 +70,74 @@ static int odb_source_files_create_on_disk(struct odb_source *source)
strbuf_addf(&path, "%s/info", source->path);
safe_create_dir(source->odb->repo, path.buf, 1);

if (opts->alternates && opts->alternates->nr) {
FILE *alternates, *orig;

strbuf_reset(&path);
strbuf_addf(&path, "%s/info/alternates", source->path);

repo_hold_lock_file_for_update(source->odb->repo, &alternates_lock,
path.buf, LOCK_DIE_ON_ERROR);

alternates = fdopen_lock_file(&alternates_lock, "w");
if (!alternates) {
ret = error_errno(_("unable to fdopen alternates lockfile"));
goto out;
}

/*
* The alternates file may already exist, e.g. when it has been
* seeded from a template directory. Read any preexisting
* entries so that we don't end up writing duplicates.
*/
orig = fopen(path.buf, "r");
if (orig) {
while (strbuf_getline(&line, orig) != EOF) {
strset_add(&seen, line.buf);
fprintf(alternates, "%s\n", line.buf);
}

if (ferror(orig)) {
ret = error_errno(_("unable to read alternates file"));
fclose(orig);
goto out;
}

fclose(orig);
} else if (errno != ENOENT) {
ret = error_errno(_("unable to read alternates file"));
goto out;
}

for (size_t i = 0; i < opts->alternates->nr; i++) {
const char *alternate = opts->alternates->v[i];
if (!strset_add(&seen, alternate))
continue;
fprintf(alternates, "%s\n", alternate);
}

if (ferror(alternates)) {
ret = error_errno(_("unable to write alternates file"));
goto out;
}

if (commit_lock_file(&alternates_lock)) {
ret = error_errno(_("unable to commit alternates file"));
goto out;
}
}

/* Reprepare the object database to activate alternates. */
odb_reprepare(source->odb);

ret = 0;

out:
rollback_lock_file(&alternates_lock);
strbuf_release(&line);
strbuf_release(&path);
return 0;
strset_clear(&seen);
return ret;
}

static void odb_source_files_prepare(struct odb_source *source,
@ -251,59 +323,6 @@ static int odb_source_files_read_alternates(struct odb_source *source,
return 0;
}

static int odb_source_files_write_alternate(struct odb_source *source,
const char *alternate)
{
struct lock_file lock = LOCK_INIT;
char *path = xstrfmt("%s/%s", source->path, "info/alternates");
FILE *in, *out;
int found = 0;
int ret;

repo_hold_lock_file_for_update(source->odb->repo, &lock, path,
LOCK_DIE_ON_ERROR);
out = fdopen_lock_file(&lock, "w");
if (!out) {
ret = error_errno(_("unable to fdopen alternates lockfile"));
goto out;
}

in = fopen(path, "r");
if (in) {
struct strbuf line = STRBUF_INIT;

while (strbuf_getline(&line, in) != EOF) {
if (!strcmp(alternate, line.buf)) {
found = 1;
break;
}
fprintf_or_die(out, "%s\n", line.buf);
}

strbuf_release(&line);
fclose(in);
} else if (errno != ENOENT) {
ret = error_errno(_("unable to read alternates file"));
goto out;
}

if (found) {
rollback_lock_file(&lock);
} else {
fprintf_or_die(out, "%s\n", alternate);
if (commit_lock_file(&lock)) {
ret = error_errno(_("unable to move new alternates file into place"));
goto out;
}
}

ret = 0;

out:
free(path);
return ret;
}

static int too_many_loose_objects(struct odb_source_files *files, int limit)
{
unsigned long loose_count;
@ -934,7 +953,6 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,
files->base.write_object_stream = odb_source_files_write_object_stream;
files->base.begin_transaction = odb_source_files_begin_transaction;
files->base.read_alternates = odb_source_files_read_alternates;
files->base.write_alternate = odb_source_files_write_alternate;
files->base.optimize = odb_source_files_optimize;
files->base.optimize_required = odb_source_files_optimize_required;
files->base.generate_pack = odb_source_files_generate_pack;

View File

@ -327,12 +327,6 @@ static int odb_source_inmemory_read_alternates(struct odb_source *source UNUSED,
return 0;
}

static int odb_source_inmemory_write_alternate(struct odb_source *source UNUSED,
const char *alternate UNUSED)
{
return error("in-memory source does not support alternates");
}

static void odb_source_inmemory_close(struct odb_source *source UNUSED)
{
}
@ -389,7 +383,6 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb)
source->base.freshen_object = odb_source_inmemory_freshen_object;
source->base.begin_transaction = odb_source_inmemory_begin_transaction;
source->base.read_alternates = odb_source_inmemory_read_alternates;
source->base.write_alternate = odb_source_inmemory_write_alternate;

return source;
}

View File

@ -990,12 +990,6 @@ static int odb_source_loose_read_alternates(struct odb_source *source UNUSED,
return 0;
}

static int odb_source_loose_write_alternate(struct odb_source *source UNUSED,
const char *alternate UNUSED)
{
return error("loose source does not support alternates");
}

static void odb_source_loose_clear_cache(struct odb_source_loose *loose)
{
oidtree_clear(loose->cache);
@ -1060,7 +1054,6 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb,
loose->base.write_object_stream = odb_source_loose_write_object_stream;
loose->base.begin_transaction = odb_source_loose_begin_transaction;
loose->base.read_alternates = odb_source_loose_read_alternates;
loose->base.write_alternate = odb_source_loose_write_alternate;

if (!is_absolute_path(loose->base.path))
chdir_notify_register(odb_source_loose_reparent, loose);

View File

@ -658,12 +658,6 @@ static int odb_source_packed_read_alternates(struct odb_source *source UNUSED,
return 0;
}

static int odb_source_packed_write_alternate(struct odb_source *source UNUSED,
const char *alternate UNUSED)
{
return error("packed backend cannot write alternates");
}

void (*report_garbage)(unsigned seen_bits, const char *path);

static void report_helper(const struct string_list *list,
@ -878,7 +872,6 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb,
packed->base.write_object_stream = odb_source_packed_write_object_stream;
packed->base.begin_transaction = odb_source_packed_begin_transaction;
packed->base.read_alternates = odb_source_packed_read_alternates;
packed->base.write_alternate = odb_source_packed_write_alternate;

if (!is_absolute_path(path))
chdir_notify_register(odb_source_packed_reparent, packed);

View File

@ -37,6 +37,15 @@ struct odb_stream;
struct strbuf;
struct strvec;

struct odb_create_on_disk_options {
/*
* Alternates that shall be written into the newly created object
* database. Whether or not this option can be handled is specific to
* the backend.
*/
const struct strvec *alternates;
};

/*
* The source is the part of the object database that stores the actual
* objects. It thus encapsulates the logic to read and write the specific
@ -107,7 +116,8 @@ struct odb_source {
* This callback may be NULL in case the source does not need any
* on-disk setup.
*/
int (*create_on_disk)(struct odb_source *source);
int (*create_on_disk)(struct odb_source *source,
const struct odb_create_on_disk_options *opts);

/*
* This callback is expected to prepare the source so that it becomes
@ -281,19 +291,6 @@ struct odb_source {
int (*read_alternates)(struct odb_source *source,
struct strvec *out);

/*
* This callback is expected to persist the singular alternate passed
* to it into its list of alternates. Any pre-existing alternates are
* expected to remain active. Subsequent calls to `read_alternates` are
* thus expected to yield the pre-existing list of alternates plus the
* newly added alternate appended to its end.
*
* The callback is expected to return 0 on success, a negative error
* code otherwise.
*/
int (*write_alternate)(struct odb_source *source,
const char *alternate);

/*
* This callback is expected to optimize the object database source.
* Returns 0 on success, a negative error code otherwise.
@ -378,11 +375,12 @@ static inline void odb_source_close(struct odb_source *source)
* Create on-disk data structures that are required for this source to operate
* correctly. Returns 0 on success, a negative error code otherwise.
*/
static inline int odb_source_create_on_disk(struct odb_source *source)
static inline int odb_source_create_on_disk(struct odb_source *source,
const struct odb_create_on_disk_options *opts)
{
if (!source->create_on_disk)
return 0;
return source->create_on_disk(source);
return source->create_on_disk(source, opts);
}

/*
@ -534,19 +532,6 @@ static inline int odb_source_read_alternates(struct odb_source *source,
return source->read_alternates(source, out);
}

/*
* Write and persist a new alternate object database source for the given
* source. Any preexisting alternates are expected to stay valid, and the new
* alternate shall be appended to the end of the list.
*
* Returns 0 on success, a negative error code otherwise.
*/
static inline int odb_source_write_alternate(struct odb_source *source,
const char *alternate)
{
return source->write_alternate(source, alternate);
}

/*
* Create a new transaction that can be used to write objects into a temporary
* staging area. The objects will only be persisted when the transaction is

15
setup.c
View File

@ -2663,8 +2663,13 @@ static int create_default_files(struct repository *repo,
return reinit;
}

static void create_object_database(struct repository *repo)
void create_object_database(struct repository *repo,
const struct strvec *alternates)
{
struct odb_create_on_disk_options opts = {
.alternates = alternates,
};

/*
* Create the "objects" directory in the common directory. This is done
* so that the repository can be discovered regardless of the backend
@ -2684,7 +2689,7 @@ static void create_object_database(struct repository *repo)

repo->objects = odb_new(repo, ODB_NEW_HONOR_ENV);

if (odb_source_create_on_disk(repo->objects->sources) < 0)
if (odb_source_create_on_disk(repo->objects->sources, &opts) < 0)
die(_("failed creating object database"));
}

@ -2845,7 +2850,8 @@ int init_db(struct repository *repo,
const char *template_dir, int hash,
enum ref_storage_format ref_storage_format,
const char *initial_branch,
int init_shared_repository, unsigned int flags)
int init_shared_repository,
enum init_db_flags flags)
{
int reinit;
int exist_ok = flags & INIT_DB_EXIST_OK;
@ -2919,7 +2925,8 @@ int init_db(struct repository *repo,

if (!(flags & INIT_DB_SKIP_REFDB))
create_reference_database(repo, initial_branch, flags & INIT_DB_QUIET);
create_object_database(repo);
if (!(flags & INIT_DB_SKIP_ODB))
create_object_database(repo, NULL);

startup_info->have_repository = 1;


15
setup.h
View File

@ -257,9 +257,12 @@ int apply_repository_format(struct repository *repo,

const char *get_template_dir(const char *option_template);

#define INIT_DB_QUIET (1 << 0)
#define INIT_DB_EXIST_OK (1 << 1)
#define INIT_DB_SKIP_REFDB (1 << 2)
enum init_db_flags {
INIT_DB_QUIET = (1 << 0),
INIT_DB_EXIST_OK = (1 << 1),
INIT_DB_SKIP_REFDB = (1 << 2),
INIT_DB_SKIP_ODB = (1 << 3),
};

int init_db(struct repository *repo,
const char *git_dir,
@ -267,13 +270,15 @@ int init_db(struct repository *repo,
const char *worktree,
const char *template_dir, int hash_algo,
enum ref_storage_format ref_storage_format,
const char *initial_branch, int init_shared_repository,
unsigned int flags);
const char *initial_branch,
int init_shared_repository,
enum init_db_flags flags);
void initialize_repository_version(struct repository *repo,
int hash_algo,
enum ref_storage_format ref_storage_format,
int reinit);
void create_reference_database(struct repository *repo, const char *initial_branch, int quiet);
void create_object_database(struct repository *repo, const struct strvec *alternates);

/*
* NOTE NOTE NOTE!!

View File

@ -383,4 +383,29 @@ test_expect_success 'dissociate from repo with commit graph' '
git clone --no-local --reference graph.git --dissociate orig clone
'

test_expect_success 'local clone from linked worktree carries over alternates' '
rm -fr base derived derived-wt dst expect &&
git init base &&
test_commit -C base one &&
git clone --shared base derived &&
git -C derived worktree add ../derived-wt &&
git clone derived-wt dst &&
echo "$(pwd)/base/.git/objects" >expect &&
test_cmp expect dst/.git/objects/info/alternates &&
git -C dst fsck
'

test_expect_success 'local clone from linked worktree resolves relative alternates' '
rm -fr base derived derived-wt dst expect &&
git init base &&
test_commit -C base one &&
git clone --shared base derived &&
echo "../../../base/.git/objects" >derived/.git/objects/info/alternates &&
git -C derived worktree add ../derived-wt &&
git clone derived-wt dst &&
echo "$(pwd)/base/.git/objects" >expect &&
test_cmp expect dst/.git/objects/info/alternates &&
git -C dst fsck
'

test_done