From 7e5a5a79471dd28d402d2d7f791ce94b44ef895e Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 16 Jul 2025 11:49:46 -0700 Subject: [PATCH] feat: Better support for pagination in gh branch dropdown (#430) * feat: Better support for pagination in gh branch dropdown * cr * cr --- .../repo-branch-selectors/branch-selector.tsx | 106 +++++++++++++++--- apps/web/src/hooks/useGitHubApp.ts | 42 ++++++- apps/web/src/utils/github.ts | 44 +++++++- 3 files changed, 172 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx index c2fa38e0..383bd8ee 100644 --- a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx +++ b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx @@ -18,6 +18,8 @@ import { useState, useEffect } from "react"; import { useGitHubAppProvider } from "@/providers/GitHubApp"; import { GitBranch, Shield } from "lucide-react"; import { TargetRepository } from "@open-swe/shared/open-swe/types"; +import { Branch } from "@/utils/github"; +import { toast } from "sonner"; interface BranchSelectorProps { disabled?: boolean; @@ -27,6 +29,21 @@ interface BranchSelectorProps { streamTargetRepository?: TargetRepository; } +const findDefaultBranch = ( + branches: Branch[], + defaultBranch: string | null, +) => { + const actualDefaultBranch = defaultBranch + ? branches.find((branch) => branch.name === defaultBranch) + : null; + + return actualDefaultBranch + ? actualDefaultBranch.name + : branches.length > 0 + ? branches[0].name + : null; +}; + export function BranchSelector({ disabled = false, placeholder = "Select a branch...", @@ -35,6 +52,8 @@ export function BranchSelector({ streamTargetRepository, }: BranchSelectorProps) { const [open, setOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [isSearching, setIsSearching] = useState(false); const { branches, branchesLoading, @@ -45,6 +64,7 @@ export function BranchSelector({ branchesHasMore, branchesLoadingMore, loadMoreBranches, + searchForBranch, defaultBranch, } = useGitHubAppProvider(); @@ -61,19 +81,16 @@ export function BranchSelector({ selectedBranch && branches.some((branch) => branch.name === selectedBranch); - // Auto-select default branch if no branch is selected OR if the selected branch doesn't exist in this repo - if (!selectedBranch || !currentBranchExists) { - // Try to find the repository's actual default branch first - const actualDefaultBranch = defaultBranch - ? branches.find((branch) => branch.name === defaultBranch) - : null; - - if (actualDefaultBranch) { - setSelectedBranch(actualDefaultBranch.name); - } else if (branches.length > 0) { - // If default branch doesn't exist in branches list, select the first available branch - setSelectedBranch(branches[0].name); - } + if (selectedBranch && !currentBranchExists) { + searchForBranch(selectedBranch).then((b) => { + if (b) { + // branch was found after search. can return early + return; + } + setSelectedBranch(findDefaultBranch(branches, defaultBranch)); + }); + } else if (!selectedBranch || !currentBranchExists) { + setSelectedBranch(findDefaultBranch(branches, defaultBranch)); } } }, [ @@ -92,6 +109,20 @@ export function BranchSelector({ setOpen(false); }; + const handleSearchForBranch = async () => { + if (!searchQuery.trim() || !selectedRepository) return; + + setIsSearching(true); + try { + const foundBranch = await searchForBranch(searchQuery.trim()); + if (!foundBranch) { + toast.warning(`Branch "${searchQuery.trim()}" not found`); + } + } finally { + setIsSearching(false); + } + }; + if (!selectedRepository) { return ( + )} + + {branches .slice() @@ -244,6 +298,26 @@ export function BranchSelector({ ); })} + {/* Show this search button if there is a search query, and there are some results. this is for + cases when some results do show, just not the exact result the user is looking for */} + {searchQuery.trim() && + branches.some((branch) => + branch.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) && ( +
+ +
+ )} {branchesHasMore && ( void; refreshBranches: () => Promise; + searchForBranch: (branchName: string) => Promise; // Repository metadata defaultBranch: string | null; @@ -294,6 +300,39 @@ export function useGitHubApp(): UseGitHubAppReturn { } }, [branchesHasMore, branchesLoadingMore, branchesPage, fetchBranches]); + const searchForBranch = useCallback( + async (branchName: string): Promise => { + if (!selectedRepository) { + return null; + } + + try { + const branch = await searchBranch( + selectedRepository.owner, + selectedRepository.repo, + branchName, + ); + + if (branch) { + // Add the found branch to the existing branches list if it's not already there + setBranches((prev) => { + const exists = prev.some((b) => b.name === branch.name); + if (!exists) { + return [...prev, branch]; + } + return prev; + }); + } + + return branch; + } catch (error) { + console.error(`Error searching for branch ${branchName}:`, error); + return null; + } + }, + [selectedRepository?.owner, selectedRepository?.repo], + ); + // Refresh repositories when installation changes useEffect(() => { if (currentInstallationId) { @@ -483,6 +522,7 @@ export function useGitHubApp(): UseGitHubAppReturn { selectedBranch, setSelectedBranch, refreshBranches, + searchForBranch, setBranchesPage, setBranches, diff --git a/apps/web/src/utils/github.ts b/apps/web/src/utils/github.ts index 6fab6331..927be68e 100644 --- a/apps/web/src/utils/github.ts +++ b/apps/web/src/utils/github.ts @@ -187,7 +187,10 @@ export async function getRepositoryBranches( /** * Fetches a specific repository using OAuth access token */ -export async function getRepository(owner: string, repo: string) { +export async function getRepository( + owner: string, + repo: string, +): Promise { const response = await fetch( `${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}`, { @@ -203,8 +206,43 @@ export async function getRepository(owner: string, repo: string) { throw new Error(`Failed to fetch repository: ${JSON.stringify(errorData)}`); } - const data = await response.json(); - return data; + return response.json(); +} + +/** + * Searches for a specific branch by name in a repository + */ +export async function searchBranch( + owner: string, + repo: string, + branchName: string, +): Promise { + try { + const response = await fetch( + `${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches/${encodeURIComponent(branchName)}`, + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenSWE-Agent", + }, + }, + ); + + if (!response.ok) { + if (response.status === 404) { + return null; // Branch not found + } + const errorData = await response.json(); + throw new Error( + `Failed to search for branch: ${JSON.stringify(errorData)}`, + ); + } + + return response.json(); + } catch (error) { + console.error(`Error searching for branch ${branchName}:`, error); + return null; + } } /**