Skip to content

perf(git): optimize fetch for repos with many tags and branches#453

Open
zendesk-slowery23 wants to merge 2 commits into
carvel-dev:developfrom
zendesk-slowery23:git-fetch-performance-optimization
Open

perf(git): optimize fetch for repos with many tags and branches#453
zendesk-slowery23 wants to merge 2 commits into
carvel-dev:developfrom
zendesk-slowery23:git-fetch-performance-optimization

Conversation

@zendesk-slowery23

@zendesk-slowery23 zendesk-slowery23 commented Jun 24, 2026

Copy link
Copy Markdown

Problem

vendir sync is very slow for git repositories with large numbers of branches and tags. The root cause is that the git fetcher unconditionally:

  1. Set remote.origin.tagOpt = --tags, causing all tag objects to be downloaded on every fetch
  2. Did not pass a refspec to git fetch, causing all remote refs to be negotiated

On a repo with thousands of branches and tags, this turns every sync into a multi-megabyte download regardless of what ref you actually need.

Solution

When a fixed named ref is specified (tag or branch, not a SHA), use a targeted single-refspec fetch:

git fetch origin <ref> --no-tags [--depth N]

Then check out FETCH_HEAD (which git populates from a single-refspec fetch) instead of the ref name.

--tags is kept only where it's actually needed:

  • refSelection (semver matching must enumerate all tags)
  • Plain SHA refs with no OriginalRef (SHAs can't be fetched by name)

--locked path

--locked substitutes the locked SHA for the original ref before reaching the fetcher, which previously forced a full --tags fetch every time. This PR preserves the original ref in a new unexported OriginalRef field on DirectoryContentsGit (set by Lock(), not serialized to YAML). The fetcher uses it for the same targeted fetch path, then verifies the resulting commit SHA matches the lock — catching force-pushed tags.

Performance

Measured against an internal repo with thousands of branches and hundreds of tags:

Command Before After Improvement
vendir sync 43s 11s 74% faster
vendir sync --locked 1m 26s 9s 89% faster

Changes

  • pkg/vendir/config/directory.go — add OriginalRef string (unexported from YAML) to DirectoryContentsGit; set it in Lock() before overwriting Ref with the SHA
  • pkg/vendir/fetch/git/git.go — targeted refspec fetch for named refs and locked-SHA-with-OriginalRef; conditional --tags/--no-tags; post-checkout SHA verification in locked mode; isHexSHA() helper
  • pkg/vendir/fetch/git/git_test.go — unit tests for each fetch path (named tag, origin/ branch, locked SHA+OriginalRef, plain SHA, depth ordering)
  • test/e2e/git_test.goTestGitFetchOptimizations: verifies targeted fetch line in --json output, locked sync uses OriginalRef, tampered lock SHA is detected

Testing

./hack/build.sh
go test ./pkg/vendir/fetch/git/... -v
VENDIR_BINARY_PATH=./vendir go test ./test/e2e/... -v -run TestGitFetchOptimizations

🤖 Generated with Claude Code

When syncing a git source with a fixed ref (tag or branch name),
vendir previously fetched all remote refs and all tags, which is
very slow for repos with thousands of branches or hundreds of tags.

Changes:
- Named refs (tags, branches) now use a targeted refspec fetch
  (`git fetch origin <ref> --no-tags`) and check out FETCH_HEAD,
  avoiding negotiation of every remote ref.
- `--no-tags` tagOpt is set for named refs; `--tags` is kept only
  for RefSelection (semver matching) and bare SHAs.
- In `--locked` mode, `DirectoryContentsGit.Lock()` now preserves
  the original ref in a new `OriginalRef` field (not serialized)
  before overwriting `Ref` with the locked SHA. The fetcher uses
  `OriginalRef` for the targeted fetch, then verifies the resulting
  commit matches the locked SHA — catching force-pushed tags.
- Plain SHA refs without an OriginalRef (edge case) fall back to
  the previous full fetch with `--tags`.

Observed improvement on a repo with thousands of branches:
  vendir sync:          43s → 11s  (74% faster)
  vendir sync --locked: 86s →  9s  (89% faster)

Adds unit tests covering each fetch path and e2e tests verifying
the targeted fetch behaviour and tampered-lock detection.

Signed-off-by: Steve Lowery <steve.lowery@zendesk.com>
@zendesk-slowery23 zendesk-slowery23 changed the title Optimize git fetch for repos with many tags and branches perf(git): optimize fetch for repos with many tags and branches Jun 24, 2026
if hasUser {
password := string(secret.Data[
ctlconf.SecretK8sCorev1BasicAuthPasswordKey])
password := string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result of go fmt from hack/build.sh

@zendesk-slowery23
zendesk-slowery23 marked this pull request as ready for review June 24, 2026 21:32
Copilot AI review requested due to automatic review settings June 24, 2026 21:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR speeds up vendir sync for git sources by avoiding full ref negotiation and unconditional tag downloads when only a single named ref is needed, and adds test coverage around the new fetch behaviors (including --locked).

Changes:

  • Add an internal OriginalRef field to preserve the pre-lock ref name for optimized fetching in --locked mode.
  • Update the git fetcher to use targeted single-refspec fetches and conditionally disable tag fetching.
  • Add unit + e2e tests validating fetch command shapes and locked-SHA mismatch detection.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/vendir/fetch/git/git.go Implements targeted fetch logic, conditional --tags/--no-tags, FETCH_HEAD checkout handling, and locked SHA verification.
pkg/vendir/config/directory.go Adds OriginalRef and sets it during Lock() before overwriting Ref with the locked SHA.
pkg/vendir/fetch/git/git_test.go Adds unit tests covering the new fetch paths and helper utilities for command assertions.
test/e2e/git_test.go Adds an end-to-end test asserting optimized fetch behavior and tampered lock detection.
pkg/vendir/fetch/http/sync.go Minor formatting change in basic auth password extraction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/vendir/fetch/git/git.go
Comment thread pkg/vendir/fetch/git/git.go Outdated
Comment thread pkg/vendir/fetch/git/git.go
Comment thread test/e2e/git_test.go Outdated
Comment thread pkg/vendir/fetch/git/git_test.go Outdated
Comment thread pkg/vendir/fetch/git/git_test.go
- Strip origin/ prefix from OriginalRef before using it as a fetch
  refspec in locked mode (origin/main is not a valid remote refspec)
- In locked mode, verify FETCH_HEAD^{commit} matches the locked SHA
  before checkout, then check out the SHA directly rather than
  FETCH_HEAD; this ensures mutable refs like origin/main stay pinned
  to the locked commit rather than tracking the branch tip
- Accept uppercase hex in isHexSHA (git SHAs are case-insensitive)
- Fix e2e test bare-fetch assertion (needle had a spurious \n that
  prevented it from ever triggering)
- Add unit test covering origin/ prefix stripping in locked mode
- Update locked-mode unit test to match revised checkout behavior

Signed-off-by: Steve Lowery <steve.lowery@zendesk.com>
@zendesk-slowery23

Copy link
Copy Markdown
Author

Are any of the maintainers available to approve the workflows for my PR and/or to discuss it? We use vendir pretty heavily and this performance improvement will have a big boost in our engineer's productivity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants