From: "Kristofer Karlsson via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Derrick Stolee <stolee@gmail.com>, Taylor Blau <me@ttaylorr.com>,
Jeff King <peff@peff.net>, Patrick Steinhardt <ps@pks.im>,
Kristofer Karlsson <krka@spotify.com>,
Kristofer Karlsson <krka@spotify.com>
Subject: [PATCH v3 0/2] fetch: write commit-graph using updated refs only
Date: Wed, 07 Oct 2026 14:22:55 +0000 [thread overview]
Message-ID: <pull.2239.v3.git.1791382977.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2239.git.1790930019.gitgitgadget@gmail.com>
When fetch.writeCommitGraph is enabled, the commit-graph is rebuilt from all
reachable refs after every fetch. This is unnecessarily expensive on
repositories with many refs, since add_ref_to_set() validates each ref
against the odb.
This series optimizes the commit-graph write by using only the newly updated
refs as seeds instead of scanning all refs. Since fetch writes the
commit-graph in split mode, the newly fetched history is added as a new
layer on top of the existing chain. A three-mode enum (REACHABLE / TIPS /
SKIP) makes the policy explicit:
* No-op fetch: skip the commit-graph write entirely
* Updated refs + existing graph: write incrementally from updated tips only
* No existing graph or multi-remote fetch: fall back to full reachable scan
Patch 1 adds a commit-info subcommand to test-tool read-graph for verifying
graph contents in tests.
Patch 2 implements the optimization in builtin/fetch.c with tests covering
the incremental, unrelated-commit, no-op, fallback, and shallow-rejected
cases.
Benchmark on a synthetic setup: git.git with 200K extra packed refs (~206K
total), a local file:// remote, an existing split commit-graph and a warm
page cache. Times are the median of 9 runs of the trace2 region
fetch/write-commit-graph:
scenario before after
no-op fetch 380 ms (skipped)
1 ref updated 357 ms 9.3 ms
10 refs updated 359 ms 8.9 ms
Changes since v2:
* Trim the commit message: inline the commit references, keep the
explanation of why the incremental write relies on split mode, and drop
the paragraphs that only restated the diff (collecting the tips,
auto-followed tags, skipping shallow-rejected refs).
* Reword the comment on skipping shallow-rejected refs without the
reference to store_updated_refs().
* Simplify the t5537 test by using "test_commit -C ... --no-tag" instead of
subshells.
Changes since v1:
* Explain in the commit message that fetch writes in split mode, so the new
tips are added as a new layer on top of the existing chain, and that the
incremental path relies on this (a non-split write would replace the
graph with only the closure of the seeds).
* Extend the incremental test to check that a local-only commit, which was
in the graph before the fetch but is not reachable from the fetched tips,
is still in the graph afterwards.
* Add benchmark numbers to the commit message.
* Add a comment explaining why shallow-rejected refs are skipped when
collecting the updated tips (like store_updated_refs(), since their
history is incomplete), and mention it in the commit message.
* Add a test in t5537 for a fetch with fetch.writeCommitGraph where a ref
is rejected because it would require changes to .git/shallow. Without the
check, the commit-graph write dies on the missing parent.
Kristofer Karlsson (2):
test-tool read-graph: add commit-info subcommand
fetch: write commit-graph using updated refs only
builtin/fetch.c | 69 +++++++++++++++++++++++++++++++++-----
commit-graph.c | 2 +-
commit-graph.h | 1 +
t/helper/test-read-graph.c | 23 ++++++++++++-
t/t5510-fetch.sh | 59 ++++++++++++++++++++++++++++++++
t/t5537-fetch-shallow.sh | 22 ++++++++++++
6 files changed, 165 insertions(+), 11 deletions(-)
base-commit: 0f8e75abebff0877cae681a3d5ff31ac47f54220
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2239%2Fspkrka%2Fkrka%2Fincremental-commit-graph-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2239/spkrka/krka/incremental-commit-graph-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2239
Range-diff vs v2:
1: 36cc3ff3b3 = 1: 36cc3ff3b3 test-tool read-graph: add commit-info subcommand
2: 7507354cc9 ! 2: 01da9857bc fetch: write commit-graph using updated refs only
@@ Metadata
## Commit message ##
fetch: write commit-graph using updated refs only
- When fetch.writeCommitGraph was introduced in
-
- 50f26bd035 (fetch: add fetch.writeCommitGraph config
- setting, 2019-09-02),
-
- the stated goal was to stay updated with the latest commits after
- fetching new objects. The implementation used
- write_commit_graph_reachable() because it was the only API available,
- but two things have changed since then:
+ When fetch.writeCommitGraph was introduced in 50f26bd035 (fetch: add
+ fetch.writeCommitGraph config setting, 2019-09-02), the stated goal
+ was to stay updated with the latest commits after fetching new
+ objects. The implementation used write_commit_graph_reachable()
+ because it was the only API available, but two things have changed
+ since then:
1. write_commit_graph() was added, and it accepts an explicit set of
commits as seeds, enabling more targeted commit-graph updates.
2. The ref-scanning callback add_ref_to_set() became more expensive
- in
- 630cd5194e (commit-graph.c: peel refs in 'add_ref_to_set',
- 2020-07-22)
- when it started to validate the refs against the odb
+ in 630cd5194e (commit-graph.c: peel refs in 'add_ref_to_set',
+ 2020-07-22) when it started to validate the refs against the odb
for correctness. On a repository with many refs, this makes the
full reachable scan unnecessarily costly for a targeted fetch.
@@ Commit message
(since that would require propagating the set of refs across process
boundaries).
- Since do_fetch() already knows which refs were updated, collect them
- into an oidset and then pass them directly to write_commit_graph().
- fetch always writes the commit-graph in split mode, so this adds a
- new layer on top of the existing chain rather than replacing it:
- close_reachable() walks from the updated tips and stops at commits
- already present in the graph, so the new layer only contains the
- newly fetched history, and commits covered by the existing layers
- remain covered. This relies on split mode; a non-split write would
- replace the graph with just the closure of the seeds.
-
- The reachability closure also covers auto-followed tags, since their
- targets are reachable from the fetched tips that caused them to be
- auto-followed.
-
- Refs that are rejected because they would require changes to
- .git/shallow are skipped, just like store_updated_refs() does. Their
- objects are received but their history is incomplete, so walking from
- them would make the commit-graph write fail.
+ This relies on the commit-graph write being additive, keeping the
+ commits that are already in the graph. fetch already operates in
+ this mode (COMMIT_GRAPH_WRITE_SPLIT) and now that becomes
+ required for correctness. Without that mode, the write would
+ replace the commit-graph and lose other commits.
After fetch_one() returns, call prepare_commit_graph() (which is
made non-static by this commit) to determine the graph-write mode:
@@ builtin/fetch.c: out:
+ for (rm = ref_map; rm; rm = rm->next) {
+ struct commit *commit;
+ /*
-+ * Like store_updated_refs(), skip shallow-rejected refs:
-+ * they are not stored, and their history is incomplete.
++ * Shallow-rejected refs are not stored and their history
++ * is incomplete, so skip them.
+ */
+ if (rm->status == REF_STATUS_REJECT_SHALLOW)
+ continue;
@@ t/t5537-fetch-shallow.sh: test_expect_success 'fetch that requires changes in .g
+test_expect_success 'fetch.writeCommitGraph skips refs that require changes in .git/shallow' '
+ git clone --no-local --depth=2 .git shallow-graph &&
-+ (
-+ cd shallow-graph &&
-+ git checkout --orphan no-shallow &&
-+ commit no-shallow
-+ ) &&
++ 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/*" &&
-+ (
-+ cd shallow-graph &&
-+ commit no-shallow-2
-+ ) &&
++ test_commit -C shallow-graph --no-tag no-shallow-2 &&
+ rejected=$(git -C shallow-graph rev-parse main) &&
+ (
+ cd notshallow-graph &&
--
gitgitgadget
next prev parent reply other threads:[~2026-10-07 14:23 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 8:33 [PATCH 0/2] fetch: write commit-graph using updated refs only Kristofer Karlsson via GitGitGadget
2026-10-02 8:33 ` [PATCH 1/2] test-tool read-graph: add commit-info subcommand Kristofer Karlsson via GitGitGadget
2026-10-02 8:33 ` [PATCH 2/2] fetch: write commit-graph using updated refs only Kristofer Karlsson via GitGitGadget
2026-10-02 11:22 ` Patrick Steinhardt
2026-10-02 12:40 ` Kristofer Karlsson
2026-10-05 6:27 ` Patrick Steinhardt
2026-10-05 14:47 ` Kristofer Karlsson
2026-10-06 9:46 ` [PATCH v2 0/2] " Kristofer Karlsson via GitGitGadget
2026-10-06 9:46 ` [PATCH v2 1/2] test-tool read-graph: add commit-info subcommand Kristofer Karlsson via GitGitGadget
2026-10-06 9:46 ` [PATCH v2 2/2] fetch: write commit-graph using updated refs only Kristofer Karlsson via GitGitGadget
2026-10-07 6:39 ` Patrick Steinhardt
2026-10-07 7:33 ` Kristofer Karlsson
2026-10-07 14:22 ` Kristofer Karlsson via GitGitGadget [this message]
2026-10-07 14:22 ` [PATCH v3 1/2] test-tool read-graph: add commit-info subcommand Kristofer Karlsson via GitGitGadget
2026-10-07 14:22 ` [PATCH v3 2/2] fetch: write commit-graph using updated refs only Kristofer Karlsson via GitGitGadget
2026-10-08 6:01 ` Patrick Steinhardt
2026-10-08 6:49 ` Kristofer Karlsson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2239.v3.git.1791382977.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=krka@spotify.com \
--cc=me@ttaylorr.com \
--cc=peff@peff.net \
--cc=ps@pks.im \
--cc=stolee@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.