Merge branch 'kk/fetch-write-commit-graph-incremental' into next

The fetch.writeCommitGraph behavior has been optimized to
incrementally update the commit graph using only the newly fetched
commits as seeds.  This avoids a full reachability scan of all
references and drastically reduces the post-fetch commit-graph
update time for repositories with many references.

* kk/fetch-write-commit-graph-incremental:
  fetch: write commit-graph using updated refs only
  test-tool read-graph: add commit-info subcommand
next
Junio C Hamano 2026-10-12 08:16:11 -07:00
commit a3a4e0019e
6 changed files with 165 additions and 11 deletions

View File

@ -1911,10 +1911,34 @@ out:
return retcode;
}

static void collect_updated_tips(struct oidset *tips, struct ref *ref_map)
{
struct ref *rm;
for (rm = ref_map; rm; rm = rm->next) {
struct commit *commit;
/*
* Shallow-rejected refs are not stored and their history
* is incomplete, so skip them.
*/
if (rm->status == REF_STATUS_REJECT_SHALLOW)
continue;
if (is_null_oid(&rm->old_oid))
continue;
if (rm->peer_ref &&
oideq(&rm->old_oid, &rm->peer_ref->old_oid))
continue;
commit = lookup_commit_reference_gently(the_repository,
&rm->old_oid, 1);
if (commit)
oidset_insert(tips, &commit->object.oid);
}
}

static int do_fetch(struct transport *transport,
struct refspec *rs,
const struct fetch_config *config,
struct list_objects_filter_options *filter_options)
struct list_objects_filter_options *filter_options,
struct oidset *updated_tips)
{
struct ref_transaction *transaction = NULL;
struct ref *ref_map = NULL;
@ -2110,6 +2134,8 @@ static int do_fetch(struct transport *transport,

commit_fetch_head(&fetch_head);

collect_updated_tips(updated_tips, ref_map);

if (set_upstream) {
struct branch *branch = branch_get("HEAD");
struct ref *rm;
@ -2426,7 +2452,8 @@ static inline void fetch_one_setup_partial(struct remote *remote,
static int fetch_one(struct remote *remote, int argc, const char **argv,
int prune_tags_ok, int use_stdin_refspecs,
const struct fetch_config *config,
struct list_objects_filter_options *filter_options)
struct list_objects_filter_options *filter_options,
struct oidset *updated_tips)
{
struct refspec rs = REFSPEC_INIT_FETCH(the_hash_algo);
int i;
@ -2493,7 +2520,8 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
sigchain_push_common(unlock_pack_on_signal);
atexit(unlock_pack_atexit);
sigchain_push(SIGPIPE, SIG_IGN);
exit_code = do_fetch(gtransport, &rs, config, filter_options);
exit_code = do_fetch(gtransport, &rs, config, filter_options,
updated_tips);
sigchain_pop(SIGPIPE);
refspec_clear(&rs);
transport_disconnect(gtransport);
@ -2535,6 +2563,12 @@ int cmd_fetch(int argc,
int negotiate_only = 0;
int porcelain = 0;
int i;
enum {
GRAPH_WRITE_REACHABLE,
GRAPH_WRITE_TIPS,
GRAPH_WRITE_SKIP,
} graph_write_mode = GRAPH_WRITE_REACHABLE;
struct oidset updated_tips = OIDSET_INIT;

struct option builtin_fetch_options[] = {
OPT__VERBOSITY(&verbosity),
@ -2822,7 +2856,13 @@ int cmd_fetch(int argc,
}
trace2_region_enter("fetch", "fetch-one", the_repository);
result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,
&config, &filter_options);
&config, &filter_options, &updated_tips);
if (prepare_commit_graph(the_repository)) {
if (oidset_size(&updated_tips))
graph_write_mode = GRAPH_WRITE_TIPS;
else
graph_write_mode = GRAPH_WRITE_SKIP;
}
trace2_region_leave("fetch", "fetch-one", the_repository);
} else {
int max_children = max_jobs;
@ -2899,11 +2939,21 @@ int cmd_fetch(int argc,
if (progress)
commit_graph_flags |= COMMIT_GRAPH_WRITE_PROGRESS;

trace2_region_enter("fetch", "write-commit-graph", the_repository);
write_commit_graph_reachable(the_repository->objects->sources,
commit_graph_flags,
NULL);
trace2_region_leave("fetch", "write-commit-graph", the_repository);
if (graph_write_mode != GRAPH_WRITE_SKIP) {
trace2_region_enter("fetch", "write-commit-graph",
the_repository);
if (graph_write_mode == GRAPH_WRITE_TIPS)
write_commit_graph(
the_repository->objects->sources,
NULL, &updated_tips,
commit_graph_flags, NULL);
else
write_commit_graph_reachable(
the_repository->objects->sources,
commit_graph_flags, NULL);
trace2_region_leave("fetch", "write-commit-graph",
the_repository);
}
}

if (enable_auto_gc) {
@ -2927,6 +2977,7 @@ int cmd_fetch(int argc,
}

cleanup:
oidset_clear(&updated_tips);
string_list_clear(&list, 0);
list_objects_filter_release(&filter_options);
free(config.follow_remote_head_raw);

View File

@ -733,7 +733,7 @@ struct commit_graph *read_commit_graph_one(struct odb_source *source)
* On the first invocation, this function attempts to load the commit
* graph if the repository is configured to have one.
*/
static struct commit_graph *prepare_commit_graph(struct repository *r)
struct commit_graph *prepare_commit_graph(struct repository *r)
{
struct odb_source *source;


View File

@ -31,6 +31,7 @@ struct string_list;

char *get_commit_graph_filename(struct odb_source *source);
char *get_commit_graph_chain_filename(struct odb_source *source);
struct commit_graph *prepare_commit_graph(struct repository *r);
int open_commit_graph(const char *graph_file, int *fd, struct stat *st);
int open_commit_graph_chain(const char *chain_file, int *fd, struct stat *st,
const struct git_hash_algo *hash_algo);

View File

@ -2,6 +2,9 @@

#include "test-tool.h"
#include "commit-graph.h"
#include "commit.h"
#include "hex.h"
#include "object-name.h"
#include "repository.h"
#include "odb.h"
#include "bloom.h"
@ -91,7 +94,25 @@ int cmd__read_graph(int argc, const char **argv)
dump_graph_info(graph);
else if (!strcmp(argv[1], "bloom-filters"))
dump_graph_bloom_filters(graph);
else {
else if (!strcmp(argv[1], "commit-info")) {
int i;
for (i = 2; i < argc; i++) {
struct object_id oid;
struct commit *c;

if (repo_get_oid(the_repository, argv[i], &oid))
die("not a valid object name: '%s'", argv[i]);
c = lookup_commit_in_graph(the_repository, &oid);
if (!c) {
fprintf(stderr, "%s: not in graph\n", argv[i]);
ret = 1;
continue;
}
printf("%s generation %"PRIuMAX"\n",
oid_to_hex(&oid),
(uintmax_t)commit_graph_generation(c));
}
} else {
fprintf(stderr, "unknown sub-command: '%s'\n", argv[1]);
ret = 1;
}

View File

@ -1087,6 +1087,65 @@ test_expect_success 'fetch.writeCommitGraph' '
)
'

test_expect_success 'fetch.writeCommitGraph adds fetched commits incrementally' '
git init incremental-source &&
test_commit -C incremental-source one &&
git clone incremental-source incremental-dest &&
test_commit -C incremental-dest local &&
git -C incremental-dest commit-graph write --reachable --split &&
test_commit -C incremental-source two &&
test_commit -C incremental-source three &&
(
cd incremental-dest &&
git -c fetch.writeCommitGraph=true fetch origin &&
test-tool read-graph commit-info three two local
)
'

test_expect_success 'fetch.writeCommitGraph does not add unrelated commits' '
git init unrelated-source &&
test_commit -C unrelated-source initial &&
git clone unrelated-source unrelated-dest &&
git -C unrelated-dest commit-graph write --reachable --split &&
test_commit -C unrelated-source fetched &&
(
cd unrelated-dest &&
test_env GIT_TEST_COMMIT_GRAPH=0 test_commit local-only &&
git -c fetch.writeCommitGraph=true fetch origin &&
test-tool read-graph commit-info fetched &&
test_expect_code 1 \
test-tool read-graph commit-info local-only 2>/dev/null
)
'

test_expect_success 'fetch.writeCommitGraph skips write on no-op fetch' '
git init noop-source &&
test_commit -C noop-source one &&
git clone noop-source noop-dest &&
git -C noop-dest commit-graph write --reachable --split &&
(
cd noop-dest &&
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
git -c fetch.writeCommitGraph=true fetch origin &&
test_region ! fetch write-commit-graph trace2.txt
)
'

test_expect_success 'fetch.writeCommitGraph falls back to reachable scan without existing graph' '
git init first-graph-source &&
test_commit -C first-graph-source base &&
git clone first-graph-source first-graph-dest &&
test_commit -C first-graph-source fetched &&
(
cd first-graph-dest &&
test_commit local &&
rm -rf .git/objects/info/commit-graphs &&
rm -f .git/objects/info/commit-graph &&
git -c fetch.writeCommitGraph=true fetch origin &&
test-tool read-graph commit-info fetched local base
)
'

test_expect_success 'fetch.writeCommitGraph with submodules' '
test_config_global protocol.file.allow always &&
git clone dups super &&

View File

@ -135,6 +135,28 @@ test_expect_success 'fetch that requires changes in .git/shallow is filtered' '
)
'

test_expect_success 'fetch.writeCommitGraph skips refs that require changes in .git/shallow' '
git clone --no-local --depth=2 .git shallow-graph &&
git -C shallow-graph checkout --orphan no-shallow &&
test_commit -C shallow-graph --no-tag no-shallow &&
git init notshallow-graph &&
git -C notshallow-graph -c fetch.writeCommitGraph=true \
fetch ../shallow-graph/.git "refs/heads/*:refs/remotes/shallow/*" &&
test_commit -C shallow-graph --no-tag no-shallow-2 &&
rejected=$(git -C shallow-graph rev-parse main) &&
(
cd notshallow-graph &&
git -c fetch.writeCommitGraph=true \
fetch ../shallow-graph/.git "refs/heads/*:refs/remotes/shallow/*" &&
git for-each-ref --format="%(refname)" >actual.refs &&
echo refs/remotes/shallow/no-shallow >expect.refs &&
test_cmp expect.refs actual.refs &&
test-tool read-graph commit-info shallow/no-shallow &&
test_expect_code 1 \
test-tool read-graph commit-info $rejected 2>/dev/null
)
'

test_expect_success 'fetch --update-shallow' '
(
cd shallow &&