Investigate Gitaly server-side exclude support for unprotected branches pagination

Summary

The current unprotectedBranches GraphQL field (introduced in !228758 (closed)) uses client-side filtering to exclude protected branches from Gitaly's FindLocalBranches results. While this resolves the immediate performance problem of loading all branches into memory on every request, it has an inherent correctness limitation.

Problem

The current approach fetches sorted batches from Gitaly (3x page size), filters out protected branches, and loops for additional batches if filtering removed too many results. However, if limit protected branches are not found within MAX_GITALY_BATCHES * FETCH_MULTIPLIER * limit branches, the field returns has_next_page: false when there could be more unprotected branches.

With the current multipliers:

  • At the default limit=100, breaking pagination would require ~3,000 consecutive individually-protected branches (wildcard protections are not excluded)
  • At limit=10, it would take ~300

This risk is considered negligible in practice, but the correctness gap exists.

Proposed Solution

Gitaly's git for-each-ref supports the --exclude flag that allows excluding multiple references:

git for-each-ref refs/heads --exclude=refs/heads/master --exclude=refs/heads/main

Once Gitaly supports this option in its RPC, we can:

  1. Fetch protected branches with exact branch names (without wildcards)
  2. Make a call to the Gitaly RPC with an exclude parameter containing a list of these branches
  3. Gitaly will return the desired paginated list, avoiding post-processing on the Ruby side

This would eliminate the correctness issue entirely and further improve performance.

Context

/cc @freinink @vyaklushin @jwoodwardgl