Skip to content

perf(repo-brancher): skip already-synced branches via upfront ls-remote#5033

Open
petr-muller wants to merge 1 commit intoopenshift:mainfrom
petr-muller:fast-forwarder-speedup-2
Open

perf(repo-brancher): skip already-synced branches via upfront ls-remote#5033
petr-muller wants to merge 1 commit intoopenshift:mainfrom
petr-muller:fast-forwarder-speedup-2

Conversation

@petr-muller
Copy link
Copy Markdown
Member

@petr-muller petr-muller commented Mar 19, 2026

Summary

  • Run a single git ls-remote per repo upfront to get all remote ref SHAs
  • Compare current branch SHA against each future branch SHA: if they match, the branch is already synced and all git operations (init, fetch, push) are skipped entirely
  • Only enter the expensive clone + push path for branches that actually need work

Stacked on #5014 (merged).

Benchmark (20 repos, 2026-03-19)

Comparison against #5014 (--deepen + --unshallow):

Scenario Without ls-remote opt With ls-remote opt Improvement
Sync (with work) 450s 448s ~same
No-op (reconciled) 42s 7s 83% faster (6x)

The big win is in the no-op case: synced repos skip git init + git fetch + git push entirely, just doing one ls-remote per repo.

🤖 Generated with Claude Code

@openshift-ci-robot
Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: automatic mode

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 19, 2026
@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci bot commented Mar 19, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f72a1c5-280c-4caf-a18c-a8ea87aafdb1

📥 Commits

Reviewing files that changed from the base of the PR and between 41a0f9e and 86da97c.

📒 Files selected for processing (1)
  • cmd/repo-brancher/main.go

Walkthrough

Defers local repository initialization and per-config fetches by bulk-fetching remote branch SHAs via a new lsRemoteRefs(); tracks processed org/repo@branch entries to skip duplicates; compares remote SHAs to the current branch, errors if current ref is missing, and only initializes git when work is required (requires --confirm to proceed).

Changes

Cohort / File(s) Summary
Git operation refactoring & duplicate-skipping
cmd/repo-brancher/main.go
Added lsRemoteRefs(logger, remote) (map[string]string, error) with retry/backoff to parse git ls-remote --heads. Introduced seenBranches tracking keyed by org/repo@branch to skip duplicate processing. Replaced per-config upfront repoDir creation, git init, and shallow fetch with deferred initialization; perform git setup only when at least one future branch SHA differs and --confirm is set. Added error handling when the current branch ref is missing on the remote. Switched per-future logging to futureLogger and adjusted control flow to create repoDir and gitCmd only when needed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci bot commented Mar 19, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: petr-muller
Once this PR has been reviewed and has the lgtm label, please assign prucek for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@petr-muller petr-muller force-pushed the fast-forwarder-speedup-2 branch from 8a86f04 to 62bd257 Compare March 20, 2026 15:50
@petr-muller petr-muller force-pushed the fast-forwarder-speedup-2 branch from 62bd257 to 41a0f9e Compare April 4, 2026 19:41
@petr-muller petr-muller marked this pull request as ready for review April 6, 2026 21:16
Copilot AI review requested due to automatic review settings April 6, 2026 21:16
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Apr 6, 2026
@petr-muller
Copy link
Copy Markdown
Member Author

/hold

Still need to look & polish but should be ready for CI & reviews

@openshift-ci openshift-ci bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Apr 6, 2026
@openshift-ci openshift-ci bot requested review from Prucek and liangxia April 6, 2026 21:17
Copy link
Copy Markdown

Copilot AI left a comment

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 optimizes repo-brancher by avoiding expensive local git operations when future release branches are already synced to the current branch, using a single upfront git ls-remote per repo to compare remote SHAs.

Changes:

  • Add an upfront ls-remote call to fetch all remote refs and compute which future branches actually need work.
  • Skip git init/fetch/push entirely when all future branches are already up to date.
  • Add lsRemoteRefs helper with retry/backoff to parse ls-remote output into a ref→SHA map.

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

Comment thread cmd/repo-brancher/main.go
}

// Only init and fetch if there is actual work to do.
repoDir := path.Join(gitDir, repoInfo.Org, repoInfo.Repo)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

repoDir is built with path.Join, which is intended for slash-separated URL paths and can produce non-native paths on Windows. The rest of this repo typically uses filepath.Join for filesystem paths (e.g., cmd/private-org-sync/main.go:313), so this should likely use filepath.Join and update imports accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is correct in general, but this tool only runs in Linux CI containers, so path.Join works fine here. The existing code before this PR already used path.Join for the same purpose, so changing it would be out of scope.

Comment thread cmd/repo-brancher/main.go Outdated
var err error
sleepyTime := time.Second
for i := 0; i < 3; i++ {
c := exec.Command("git", "ls-remote", remote.String())
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

git ls-remote without filters returns all refs (including tags), which can be unnecessarily slow and memory-heavy for large repos. Since this code only needs branch heads, consider adding --heads (and/or --refs) to reduce the amount of data fetched and parsed.

Suggested change
c := exec.Command("git", "ls-remote", remote.String())
c := exec.Command("git", "ls-remote", "--heads", "--refs", remote.String())

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, fixed — added --heads to only fetch branch refs.

Comment thread cmd/repo-brancher/main.go
Comment on lines +263 to +267
b, err = c.CombinedOutput()
if err == nil {
break
}
logger.WithError(err).Debugf("ls-remote failed (attempt %d/3), retrying...", i+1)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

When ls-remote fails, the retry log and final error drop the command output (b), which makes diagnosing auth/host/key issues difficult. Consider including (possibly truncated) stdout/stderr in the debug log and/or the returned error, similar to how gitCmdFunc wraps errors with output.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The remote URL contains credentials, and this function runs outside of gitCmdFunc which handles sanitization. Including raw command output here risks leaking the token into logs. The error from exec.Command already includes the exit code, and the debug-level retry log captures the error — that should be sufficient for diagnosing issues.

Comment thread cmd/repo-brancher/main.go
Comment on lines +254 to +256
// lsRemoteRefs runs git ls-remote and returns a map of ref -> SHA.
// This does not require a local repo — git ls-remote works without one.
func lsRemoteRefs(logger *logrus.Entry, remote *url.URL) (map[string]string, error) {
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

New helper lsRemoteRefs introduces non-trivial parsing/retry behavior but has no unit tests. Since this package already has tests for related git helpers (pushBranch, fetchDeeper, fetchUnshallow), it would be good to add tests for lsRemoteRefs parsing (e.g., heads vs HEAD lines, empty output, malformed lines) and error propagation.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The parsing logic is trivial (strings.Fields on ls-remote output), and the retry/backoff pattern is the same one used in gitCmdFunc which is also untested at that level. Unit testing this would require mocking exec.Command which adds complexity without much value — the interesting behavior (correct skipping of synced branches) is better validated by e2e tests against real repos.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/repo-brancher/main.go`:
- Around line 262-279: The git ls-remote call is returning all refs (including
tags) but the code only needs branch refs; change the exec.Command invocation
that builds c (currently exec.Command("git", "ls-remote", remote.String())) to
pass the --heads flag (exec.Command("git", "ls-remote", "--heads",
remote.String())) so only refs/heads/* are returned, leaving the subsequent
parsing of parts and population of refs[parts[1]] unchanged.
- Around line 192-194: The current os.MkdirAll(repoDir, 0775) error path calls
logger.WithError(err).Fatal which exits the process; change this to log the
error (e.g., logger.WithError(err).Error or .Warn), append the current repo's
config identifier to the shared failed configs list (e.g., failedConfigs or
failed list used for the final failed-config summary), and return the error from
the function instead of calling Fatal so the caller can continue processing
remaining repos and deferred temp-dir cleanup still runs. Specifically, replace
logger.WithError(err).Fatal("could not ensure git dir existed") with a non-fatal
log, push the config (the variable that represents the repo config) onto the
failed list, and return err.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d748d4b8-95a2-47bb-adbd-cabacc37f2cf

📥 Commits

Reviewing files that changed from the base of the PR and between 52ff394 and 41a0f9e.

📒 Files selected for processing (1)
  • cmd/repo-brancher/main.go

Comment thread cmd/repo-brancher/main.go
Comment on lines +192 to +194
if err := os.MkdirAll(repoDir, 0775); err != nil {
logger.WithError(err).Fatal("could not ensure git dir existed")
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n cmd/repo-brancher/main.go | head -250 | tail -100

Repository: openshift/ci-tools

Length of output: 3695


🏁 Script executed:

cat -n cmd/repo-brancher/main.go | head -150

Repository: openshift/ci-tools

Length of output: 5277


Don't terminate the whole run from one repo's MkdirAll failure.

Calling Fatal() at line 193 exits the entire program, bypassing the deferred temp-dir cleanup (lines 99–103) and the final failed-config summary (lines 244–247). This also prevents remaining repos from being processed. Log the error, append the config to the failed list, and return the error instead.

🛠️ Proposed fix
 		repoDir := path.Join(gitDir, repoInfo.Org, repoInfo.Repo)
 		if err := os.MkdirAll(repoDir, 0775); err != nil {
-			logger.WithError(err).Fatal("could not ensure git dir existed")
-			return nil
+			logger.WithError(err).Error("could not ensure git dir existed")
+			appendFailedConfig(configuration)
+			return err
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := os.MkdirAll(repoDir, 0775); err != nil {
logger.WithError(err).Fatal("could not ensure git dir existed")
return nil
repoDir := path.Join(gitDir, repoInfo.Org, repoInfo.Repo)
if err := os.MkdirAll(repoDir, 0775); err != nil {
logger.WithError(err).Error("could not ensure git dir existed")
appendFailedConfig(configuration)
return err
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/repo-brancher/main.go` around lines 192 - 194, The current
os.MkdirAll(repoDir, 0775) error path calls logger.WithError(err).Fatal which
exits the process; change this to log the error (e.g.,
logger.WithError(err).Error or .Warn), append the current repo's config
identifier to the shared failed configs list (e.g., failedConfigs or failed list
used for the final failed-config summary), and return the error from the
function instead of calling Fatal so the caller can continue processing
remaining repos and deferred temp-dir cleanup still runs. Specifically, replace
logger.WithError(err).Fatal("could not ensure git dir existed") with a non-fatal
log, push the config (the variable that represents the repo config) onto the
failed list, and return err.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid observation, but this is pre-existing behavior — the Fatal call was already there before this PR, I just moved the block lower to defer it until after the ls-remote check. Fixing it is out of scope for this change.

Comment thread cmd/repo-brancher/main.go Outdated
@petr-muller petr-muller force-pushed the fast-forwarder-speedup-2 branch from 41a0f9e to f5ff362 Compare April 6, 2026 23:51
…ches

Instead of doing git init + fetch + push for every repo and a separate
ls-remote per future branch, run a single ls-remote per repo upfront
to get all remote ref SHAs. Compare the current branch SHA against
each future branch SHA: if they match, the branch is already synced
and all git operations (init, fetch, push) are skipped entirely.

Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 8, 2026 11:40
@petr-muller petr-muller force-pushed the fast-forwarder-speedup-2 branch from f5ff362 to 86da97c Compare April 8, 2026 11:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


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

Comment thread cmd/repo-brancher/main.go
Comment on lines +203 to +204
logger.WithError(err).Fatal("could not ensure git dir existed")
return nil
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

os.MkdirAll failure is handled with logger.Fatal, which exits the process immediately (skipping deferred cleanup like the temp git dir removal) and makes the following return nil unreachable. Prefer returning the error (and letting the main error/failedConfigs path decide exit status) rather than calling Fatal inside the per-config callback.

Suggested change
logger.WithError(err).Fatal("could not ensure git dir existed")
return nil
appendFailedConfig(configuration)
return err

Copilot uses AI. Check for mistakes.
Comment thread cmd/repo-brancher/main.go
Comment on lines +278 to +279
time.Sleep(sleepyTime)
sleepyTime *= 2
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

In lsRemoteRefs, the retry loop always sleeps after a failed attempt, including after the final (3rd) attempt. This adds unnecessary delay on permanent failures; consider only sleeping when another retry will occur (e.g., guard the sleep with if i < maxAttempts-1).

Suggested change
time.Sleep(sleepyTime)
sleepyTime *= 2
if i < 2 {
time.Sleep(sleepyTime)
sleepyTime *= 2
}

Copilot uses AI. Check for mistakes.
@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci bot commented Apr 8, 2026

@petr-muller: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/breaking-changes 86da97c link false /test breaking-changes
ci/prow/images 86da97c link true /test images

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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

Labels

do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants