mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: add pagination support for GitHub repositories and branches (#122)
* Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * cleanup & fix build * fix: always get default branch for first page * cr * cr * cr * cr * cr * cr
This commit is contained in:
parent
30881a5d92
commit
69b15a9f77
11 changed files with 367 additions and 152 deletions
|
|
@ -37,6 +37,11 @@ async function handler(req: NextRequest) {
|
|||
|
||||
const targetUrl = new URL(`${GITHUB_API_URL}/${path}`);
|
||||
|
||||
// Forward query parameters from the original request
|
||||
req.nextUrl.searchParams.forEach((value, key) => {
|
||||
targetUrl.searchParams.append(key, value);
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
headers.set("Accept", "application/vnd.github.v3+json");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
|||
|
||||
/**
|
||||
* Fetches repositories accessible to the GitHub App installation
|
||||
* Requires a valid GitHub installation ID in the cookies
|
||||
* Requires a valid GitHub installation ID in the cookies. Supports pagination via 'page' query parameter.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -27,6 +27,19 @@ export async function GET(request: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
// Get pagination parameters from query string
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||
const perPage = 30; // Fixed at 30 repositories per page
|
||||
|
||||
// Validate page parameter
|
||||
if (page < 1 || isNaN(page)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid page parameter. Must be a positive integer." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Get GitHub App credentials from environment variables
|
||||
const appId = process.env.GITHUB_APP_ID;
|
||||
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY?.replace(
|
||||
|
|
@ -58,9 +71,17 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Fetch repositories accessible to this installation
|
||||
let repositories: Repository[];
|
||||
let repositoryData: {
|
||||
repositories: Repository[];
|
||||
hasMore: boolean;
|
||||
totalCount: number;
|
||||
};
|
||||
try {
|
||||
repositories = await getInstallationRepositories(installationToken);
|
||||
repositoryData = await getInstallationRepositories(
|
||||
installationToken,
|
||||
page,
|
||||
perPage,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch repositories:", error);
|
||||
return NextResponse.json(
|
||||
|
|
@ -70,7 +91,7 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Transform the response to include only the data we need
|
||||
const transformedRepos = repositories.map((repo) => ({
|
||||
const transformedRepos = repositoryData.repositories.map((repo) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
|
|
@ -81,7 +102,15 @@ export async function GET(request: NextRequest) {
|
|||
permissions: repo.permissions,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ repositories: transformedRepos });
|
||||
return NextResponse.json({
|
||||
repositories: transformedRepos,
|
||||
pagination: {
|
||||
page,
|
||||
perPage,
|
||||
hasMore: repositoryData.hasMore,
|
||||
totalCount: repositoryData.totalCount,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub repositories:", error);
|
||||
return NextResponse.json(
|
||||
|
|
|
|||
|
|
@ -6,18 +6,21 @@ import { ThreadProvider } from "@/providers/Thread";
|
|||
import { ArtifactProvider } from "@/components/thread/artifact";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import React from "react";
|
||||
import { GitHubAppProvider } from "@/providers/GitHubApp";
|
||||
|
||||
export default function DemoPage(): React.ReactNode {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading (layout)...</div>}>
|
||||
<Toaster />
|
||||
<ThreadProvider>
|
||||
<StreamProvider>
|
||||
<ArtifactProvider>
|
||||
<Thread />
|
||||
</ArtifactProvider>
|
||||
</StreamProvider>
|
||||
</ThreadProvider>
|
||||
<GitHubAppProvider>
|
||||
<ThreadProvider>
|
||||
<StreamProvider>
|
||||
<ArtifactProvider>
|
||||
<Thread />
|
||||
</ArtifactProvider>
|
||||
</StreamProvider>
|
||||
</ThreadProvider>
|
||||
</GitHubAppProvider>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { GitBranch, Shield } from "lucide-react";
|
||||
|
||||
interface BranchSelectorProps {
|
||||
|
|
@ -39,8 +39,11 @@ export function BranchSelector({
|
|||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
selectedRepository,
|
||||
branchesHasMore,
|
||||
branchesLoadingMore,
|
||||
loadMoreBranches,
|
||||
defaultBranch,
|
||||
} = useGitHubApp();
|
||||
} = useGitHubAppProvider();
|
||||
|
||||
// Auto-select default branch when repository changes and branches are loaded
|
||||
useEffect(() => {
|
||||
|
|
@ -232,6 +235,21 @@ export function BranchSelector({
|
|||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{branchesHasMore && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
loadMoreBranches();
|
||||
}}
|
||||
disabled={branchesLoadingMore}
|
||||
className="justify-center"
|
||||
>
|
||||
{branchesLoadingMore
|
||||
? "Loading more..."
|
||||
: "Load more branches"}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Check, ChevronsUpDown } from "lucide-react";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import type { Repository } from "@/utils/github";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
|
||||
|
|
@ -50,7 +50,10 @@ export function RepositorySelector({
|
|||
isLoading,
|
||||
error,
|
||||
isInstalled,
|
||||
} = useGitHubApp();
|
||||
repositoriesHasMore,
|
||||
repositoriesLoadingMore,
|
||||
loadMoreRepositories,
|
||||
} = useGitHubAppProvider();
|
||||
|
||||
const handleSelect = (repositoryKey: string) => {
|
||||
const repository = repositories.find(
|
||||
|
|
@ -210,6 +213,21 @@ export function RepositorySelector({
|
|||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{repositoriesHasMore && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
loadMoreRepositories();
|
||||
}}
|
||||
disabled={repositoriesLoadingMore}
|
||||
className="justify-center"
|
||||
>
|
||||
{repositoriesLoadingMore
|
||||
? "Loading more..."
|
||||
: "Load more repositories"}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { InstallAppButton } from "./install-app-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
|
@ -11,7 +11,7 @@ interface RepositoryListProps {
|
|||
|
||||
export function RepositoryList({ className = "" }: RepositoryListProps) {
|
||||
const { isInstalled, isLoading, error, repositories, refreshRepositories } =
|
||||
useGitHubApp();
|
||||
useGitHubAppProvider();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,14 +14,6 @@ export function useTaskPlan() {
|
|||
}
|
||||
}, [values?.plan]);
|
||||
|
||||
const handleTaskChange = (taskId: string) => {
|
||||
console.log(`Switched to task: ${taskId}`);
|
||||
};
|
||||
|
||||
const handleRevisionChange = (taskId: string, revisionIndex: number) => {
|
||||
console.log(`Task ${taskId} switched to revision ${revisionIndex}`);
|
||||
};
|
||||
|
||||
const handleEditPlanItem = (
|
||||
taskId: string,
|
||||
planItemIndex: number,
|
||||
|
|
@ -52,9 +44,6 @@ export function useTaskPlan() {
|
|||
}),
|
||||
};
|
||||
});
|
||||
console.log(
|
||||
`Edited plan item ${planItemIndex} in task ${taskId}: ${newPlan}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddPlanItem = (taskId: string, plan: string) => {
|
||||
|
|
@ -89,7 +78,6 @@ export function useTaskPlan() {
|
|||
}),
|
||||
};
|
||||
});
|
||||
console.log(`Added new plan item to task ${taskId}: ${plan}`);
|
||||
};
|
||||
|
||||
const handleDeletePlanItem = (taskId: string, planItemIndex: number) => {
|
||||
|
|
@ -116,13 +104,10 @@ export function useTaskPlan() {
|
|||
}),
|
||||
};
|
||||
});
|
||||
console.log(`Deleted plan item ${planItemIndex} from task ${taskId}`);
|
||||
};
|
||||
|
||||
return {
|
||||
taskPlan,
|
||||
handleTaskChange,
|
||||
handleRevisionChange,
|
||||
handleEditPlanItem,
|
||||
handleAddPlanItem,
|
||||
handleDeletePlanItem,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import {
|
|||
useArtifactContext,
|
||||
} from "./artifact";
|
||||
import { GitHubOAuthButton } from "../github/github-oauth-button";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import TaskList from "../task-list";
|
||||
import { ConfigurationSidebar } from "../configuration-sidebar";
|
||||
import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/use-config-store";
|
||||
|
|
@ -100,12 +100,10 @@ export function Thread() {
|
|||
const { push } = useRouter();
|
||||
const [artifactContext, setArtifactContext] = useArtifactContext();
|
||||
const [artifactOpen, closeArtifact] = useArtifactOpen();
|
||||
const { selectedRepository } = useGitHubApp();
|
||||
const { selectedRepository } = useGitHubAppProvider();
|
||||
const { getConfig } = useConfigStore();
|
||||
const {
|
||||
taskPlan,
|
||||
handleTaskChange,
|
||||
handleRevisionChange,
|
||||
handleEditPlanItem,
|
||||
handleAddPlanItem,
|
||||
handleDeletePlanItem,
|
||||
|
|
@ -437,8 +435,8 @@ export function Thread() {
|
|||
{taskPlan && (
|
||||
<TaskPlanView
|
||||
taskPlan={taskPlan}
|
||||
onTaskChange={handleTaskChange}
|
||||
onRevisionChange={handleRevisionChange}
|
||||
onTaskChange={() => {}}
|
||||
onRevisionChange={() => {}}
|
||||
onEditPlanItem={handleEditPlanItem}
|
||||
onAddPlanItem={handleAddPlanItem}
|
||||
onDeletePlanItem={handleDeletePlanItem}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,68 @@
|
|||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
interface UseGitHubAppReturn {
|
||||
// Installation and general state
|
||||
isInstalled: boolean | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Repository state and pagination
|
||||
repositories: Repository[];
|
||||
repositoriesPage: number;
|
||||
repositoriesHasMore: boolean;
|
||||
repositoriesLoadingMore: boolean;
|
||||
refreshRepositories: () => Promise<void>;
|
||||
loadMoreRepositories: () => Promise<void>;
|
||||
|
||||
// Repository selection
|
||||
selectedRepository: TargetRepository | null;
|
||||
setSelectedRepository: (repo: TargetRepository | null) => void;
|
||||
|
||||
// Branch state and pagination
|
||||
branches: Branch[];
|
||||
branchesPage: number;
|
||||
branchesHasMore: boolean;
|
||||
branchesLoading: boolean;
|
||||
branchesLoadingMore: boolean;
|
||||
branchesError: string | null;
|
||||
loadMoreBranches: () => Promise<void>;
|
||||
fetchBranches: () => Promise<void>;
|
||||
setBranchesPage: (page: number) => void;
|
||||
setBranches: (branches: Branch[]) => void;
|
||||
|
||||
// Branch selection
|
||||
selectedBranch: string | null;
|
||||
setSelectedBranch: (branch: string | null) => void;
|
||||
refreshBranches: () => Promise<void>;
|
||||
|
||||
// Repository metadata
|
||||
defaultBranch: string | null;
|
||||
}
|
||||
|
||||
export function useGitHubApp(): UseGitHubAppReturn {
|
||||
// Installation and general state
|
||||
const [isInstalled, setIsInstalled] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Repository state and pagination
|
||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||
const [repositoriesPage, setRepositoriesPage] = useState(1);
|
||||
const [repositoriesHasMore, setRepositoriesHasMore] = useState(false);
|
||||
const [repositoriesLoadingMore, setRepositoriesLoadingMore] = useState(false);
|
||||
|
||||
// Branch state and pagination
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [branchesPage, setBranchesPage] = useState(1);
|
||||
const [branchesHasMore, setBranchesHasMore] = useState(false);
|
||||
const [branchesLoading, setBranchesLoading] = useState(false);
|
||||
const [branchesLoadingMore, setBranchesLoadingMore] = useState(false);
|
||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||
|
||||
// URL state management
|
||||
const [selectedRepositoryParam, setSelectedRepositoryParam] =
|
||||
useQueryState("repo");
|
||||
const [selectedBranchParam, setSelectedBranchParam] = useQueryState("branch");
|
||||
|
|
@ -35,24 +70,34 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
// Track if auto-selection has been attempted to prevent re-triggering
|
||||
const hasAutoSelectedRef = useRef(false);
|
||||
|
||||
const selectedRepository = selectedRepositoryParam
|
||||
? (() => {
|
||||
try {
|
||||
// Parse "owner/repo" format instead of JSON
|
||||
const parts = selectedRepositoryParam.split("/");
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
owner: parts[0],
|
||||
repo: parts[1],
|
||||
branch: selectedBranchParam || undefined,
|
||||
} as TargetRepository;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
const selectedRepository = useMemo(() => {
|
||||
if (!selectedRepositoryParam) return null;
|
||||
try {
|
||||
// Parse "owner/repo" format instead of JSON
|
||||
const parts = selectedRepositoryParam.split("/");
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
owner: parts[0],
|
||||
repo: parts[1],
|
||||
branch: selectedBranchParam || undefined,
|
||||
} as TargetRepository;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [selectedRepositoryParam, selectedBranchParam]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRepository && !branchesLoading) {
|
||||
fetchBranches();
|
||||
// Reset branch pagination when repository changes
|
||||
setBranchesPage(1);
|
||||
} else if (!selectedRepository) {
|
||||
setBranches([]);
|
||||
setSelectedBranchParam(null);
|
||||
}
|
||||
}, [selectedRepository]);
|
||||
|
||||
const selectedBranch = selectedBranchParam;
|
||||
|
||||
|
|
@ -62,6 +107,8 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
if (!repo) {
|
||||
setSelectedBranchParam(null);
|
||||
setBranches([]);
|
||||
setBranchesPage(1);
|
||||
setBranchesHasMore(false);
|
||||
}
|
||||
},
|
||||
[setSelectedRepositoryParam, setSelectedBranchParam],
|
||||
|
|
@ -71,16 +118,29 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
setSelectedBranchParam(branch);
|
||||
};
|
||||
|
||||
const checkInstallation = async () => {
|
||||
setIsLoading(true);
|
||||
const checkInstallation = async (
|
||||
page: number = 1,
|
||||
append: boolean = false,
|
||||
) => {
|
||||
if (!append) setIsLoading(true);
|
||||
if (append) setRepositoriesLoadingMore(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/github/repositories");
|
||||
const response = await fetch(`/api/github/repositories?page=${page}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setRepositories(data.repositories || []);
|
||||
const newRepositories = data.repositories || [];
|
||||
|
||||
if (append) {
|
||||
setRepositories((prev) => [...prev, ...newRepositories]);
|
||||
} else {
|
||||
setRepositories(newRepositories);
|
||||
}
|
||||
|
||||
setRepositoriesPage(data.pagination?.page || page);
|
||||
setRepositoriesHasMore(data.pagination?.hasMore || false);
|
||||
setIsInstalled(true);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
|
|
@ -95,56 +155,78 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
setError("Failed to check GitHub App installation status");
|
||||
setIsInstalled(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (!append) setIsLoading(false);
|
||||
if (append) setRepositoriesLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBranches = useCallback(async () => {
|
||||
if (!selectedRepository) {
|
||||
setBranches([]);
|
||||
return;
|
||||
}
|
||||
const fetchBranches = useCallback(
|
||||
async (page: number = 1, append: boolean = false) => {
|
||||
if (!selectedRepository) {
|
||||
setBranches([]);
|
||||
setBranchesPage(1);
|
||||
setBranchesHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setBranchesLoading(true);
|
||||
setBranchesError(null);
|
||||
if (!append) setBranchesLoading(true);
|
||||
if (append) setBranchesLoadingMore(true);
|
||||
setBranchesError(null);
|
||||
|
||||
try {
|
||||
const branchData = await getRepositoryBranches(
|
||||
selectedRepository.owner,
|
||||
selectedRepository.repo,
|
||||
);
|
||||
setBranches(branchData || []);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to fetch branches";
|
||||
console.error(
|
||||
`Error fetching branches for ${selectedRepository.owner}/${selectedRepository.repo}:`,
|
||||
err,
|
||||
);
|
||||
setBranchesError(errorMessage);
|
||||
} finally {
|
||||
setBranchesLoading(false);
|
||||
try {
|
||||
const branchData = await getRepositoryBranches(
|
||||
selectedRepository.owner,
|
||||
selectedRepository.repo,
|
||||
page,
|
||||
);
|
||||
|
||||
if (append) {
|
||||
setBranches((prev) => {
|
||||
// Avoid adding duplicates
|
||||
const newBranches = branchData.branches.filter(
|
||||
(branch) => !prev.some((b) => b.name === branch.name),
|
||||
);
|
||||
return [...prev, ...newBranches];
|
||||
});
|
||||
} else {
|
||||
setBranches(branchData.branches);
|
||||
}
|
||||
|
||||
setBranchesPage(page);
|
||||
setBranchesHasMore(branchData.hasMore);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to fetch branches";
|
||||
console.error(
|
||||
`Error fetching branches for ${selectedRepository.owner}/${selectedRepository.repo}:`,
|
||||
err,
|
||||
);
|
||||
setBranchesError(errorMessage);
|
||||
} finally {
|
||||
if (!append) setBranchesLoading(false);
|
||||
if (append) setBranchesLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[selectedRepository?.owner, selectedRepository?.repo],
|
||||
);
|
||||
|
||||
// Load more functions
|
||||
const loadMoreRepositories = useCallback(async () => {
|
||||
if (repositoriesHasMore && !repositoriesLoadingMore) {
|
||||
await checkInstallation(repositoriesPage + 1, true);
|
||||
}
|
||||
}, [selectedRepository?.owner, selectedRepository?.repo]);
|
||||
}, [repositoriesHasMore, repositoriesLoadingMore, repositoriesPage]);
|
||||
|
||||
const loadMoreBranches = useCallback(async () => {
|
||||
if (branchesHasMore && !branchesLoadingMore) {
|
||||
await fetchBranches(branchesPage + 1, true);
|
||||
}
|
||||
}, [branchesHasMore, branchesLoadingMore, branchesPage, fetchBranches]);
|
||||
|
||||
useEffect(() => {
|
||||
checkInstallation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRepository) {
|
||||
fetchBranches();
|
||||
} else {
|
||||
setBranches([]);
|
||||
setSelectedBranchParam(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
selectedRepository?.owner,
|
||||
selectedRepository?.repo,
|
||||
setSelectedBranchParam,
|
||||
]);
|
||||
|
||||
// Auto-select first repository on initial page load
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -173,10 +255,16 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
]);
|
||||
|
||||
const refreshRepositories = async () => {
|
||||
// Reset pagination state on refresh
|
||||
setRepositoriesPage(1);
|
||||
setRepositoriesHasMore(false);
|
||||
await checkInstallation();
|
||||
};
|
||||
|
||||
const refreshBranches = async () => {
|
||||
// Reset pagination state on refresh
|
||||
setBranchesPage(1);
|
||||
setBranchesHasMore(false);
|
||||
await fetchBranches();
|
||||
};
|
||||
|
||||
|
|
@ -190,19 +278,41 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
: null;
|
||||
|
||||
return {
|
||||
// Installation and general state
|
||||
isInstalled,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Repository state and pagination
|
||||
repositories,
|
||||
repositoriesPage,
|
||||
repositoriesHasMore,
|
||||
repositoriesLoadingMore,
|
||||
refreshRepositories,
|
||||
loadMoreRepositories,
|
||||
|
||||
// Repository selection
|
||||
selectedRepository,
|
||||
setSelectedRepository,
|
||||
|
||||
// Branch state and pagination
|
||||
branches,
|
||||
branchesPage,
|
||||
branchesHasMore,
|
||||
branchesLoading,
|
||||
branchesLoadingMore,
|
||||
branchesError,
|
||||
loadMoreBranches,
|
||||
fetchBranches,
|
||||
|
||||
// Branch selection
|
||||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
refreshBranches,
|
||||
setBranchesPage,
|
||||
setBranches,
|
||||
|
||||
// Repository metadata
|
||||
defaultBranch,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
27
apps/web/src/providers/GitHubApp.tsx
Normal file
27
apps/web/src/providers/GitHubApp.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { createContext, useContext, ReactNode } from "react";
|
||||
|
||||
type GitHubAppContextType = ReturnType<typeof useGitHubApp>;
|
||||
|
||||
const GitHubAppContext = createContext<GitHubAppContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function GitHubAppProvider({ children }: { children: ReactNode }) {
|
||||
const value = useGitHubApp();
|
||||
return (
|
||||
<GitHubAppContext.Provider value={value}>
|
||||
{children}
|
||||
</GitHubAppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGitHubAppProvider() {
|
||||
const context = useContext(GitHubAppContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useGitHubAppProvider must be used within a GitHubAppProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
|
||||
function getBaseApiUrl(): string {
|
||||
|
|
@ -41,7 +40,7 @@ export async function getInstallationToken(
|
|||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "YourAppName",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -62,17 +61,24 @@ export async function getInstallationToken(
|
|||
*/
|
||||
export async function getInstallationRepositories(
|
||||
installationToken: string,
|
||||
): Promise<Repository[]> {
|
||||
const response = await fetch(
|
||||
"https://api.github.com/installation/repositories",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${installationToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "YourAppName",
|
||||
},
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
): Promise<{
|
||||
repositories: Repository[];
|
||||
hasMore: boolean;
|
||||
totalCount: number;
|
||||
}> {
|
||||
const url = new URL("https://api.github.com/installation/repositories");
|
||||
url.searchParams.set("page", page.toString());
|
||||
url.searchParams.set("per_page", perPage.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${installationToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "YourAppName",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
|
|
@ -82,7 +88,12 @@ export async function getInstallationRepositories(
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.repositories;
|
||||
|
||||
return {
|
||||
repositories: data.repositories,
|
||||
hasMore: data.repositories.length === perPage,
|
||||
totalCount: data.total_count,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,11 +102,9 @@ export async function getInstallationRepositories(
|
|||
export async function getRepositoryBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<Branch[]> {
|
||||
const allBranches: Branch[] = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // Maximum allowed by GitHub API
|
||||
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
): Promise<{ branches: Branch[]; hasMore: boolean; totalCount?: number }> {
|
||||
// First, get repository info to ensure we have the default branch
|
||||
|
||||
const repoResponse = await fetch(
|
||||
|
|
@ -114,52 +123,65 @@ export async function getRepositoryBranches(
|
|||
defaultBranch = repoData.default_branch;
|
||||
}
|
||||
|
||||
// Fetch all branches with pagination
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
// Fetch first 30 branches only
|
||||
const response = await fetch(
|
||||
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
const branches: Branch[] = Array.isArray(data) ? data : [];
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allBranches.push(...data);
|
||||
|
||||
// If we got less than the requested amount, we've reached the end
|
||||
if (data.length < perPage) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
if (branches.length === 0) {
|
||||
return { branches: [], hasMore: false };
|
||||
}
|
||||
|
||||
// Ensure default branch is at the beginning if it exists
|
||||
if (defaultBranch) {
|
||||
const defaultBranchIndex = allBranches.findIndex(
|
||||
if (defaultBranch && page === 1) {
|
||||
const defaultBranchIndex = branches.findIndex(
|
||||
(branch) => branch.name === defaultBranch,
|
||||
);
|
||||
if (defaultBranchIndex > 0) {
|
||||
const defaultBranchData = allBranches[defaultBranchIndex];
|
||||
allBranches.splice(defaultBranchIndex, 1);
|
||||
allBranches.unshift(defaultBranchData);
|
||||
const defaultBranchData = branches[defaultBranchIndex];
|
||||
branches.splice(defaultBranchIndex, 1);
|
||||
branches.unshift(defaultBranchData);
|
||||
} else {
|
||||
// Need to fetch default branch
|
||||
const defaultBranchResponse = await fetch(
|
||||
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches/${defaultBranch}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!defaultBranchResponse.ok) {
|
||||
const errorData = await defaultBranchResponse.json();
|
||||
throw new Error(
|
||||
`Failed to fetch default branch: ${JSON.stringify(errorData)}`,
|
||||
);
|
||||
}
|
||||
const defaultBranchData = await defaultBranchResponse.json();
|
||||
branches.unshift(defaultBranchData);
|
||||
}
|
||||
}
|
||||
|
||||
return allBranches;
|
||||
return {
|
||||
branches,
|
||||
hasMore: branches.length >= perPage,
|
||||
totalCount: branches.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue