From f989b47470a9fe817cc3975b9acac59e03d42ba1 Mon Sep 17 00:00:00 2001 From: Alex Netsch Date: Tue, 16 Dec 2025 14:21:42 +0000 Subject: [PATCH] Refactor branch fetching to use repo IDs instead of project IDs (Vibe Kanban) (#1560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * All tasks are complete. Here's a summary of the changes made: ## Summary Successfully refactored the project branches API to fetch branches using repo IDs instead of project IDs. ### Files Created - `frontend/src/hooks/useRepoBranches.ts` - New hook that fetches branches for a single repository using `repoApi.getBranches(repoId)` ### Files Modified 1. **`frontend/src/hooks/index.ts`** - Added export for `useRepoBranches` 2. **`frontend/src/components/dialogs/tasks/GitActionsDialog.tsx`** - Removed `useState` for `repoBranches` and `loadingBranches` - Removed `useEffect` that called `projectsApi.getBranches(effectiveProjectId)` - Removed `useMemo` that filtered branches by `selectedRepoId` - Now uses `useRepoBranches(selectedRepoId)` to fetch branches directly for the selected repo from the task attempt 3. **`frontend/src/components/dialogs/tasks/CreatePRDialog.tsx`** - Removed `projectId` from props interface and component destructuring - Removed `useState` for `branches` and `branchesLoading` - Removed branch-fetching logic from useEffect - Now uses `useRepoBranches(repoId)` to fetch branches directly for the specified repo - Split useEffect into two: one for form initialization, one for setting default base branch 4. **`frontend/src/components/tasks/Toolbar/GitOperations.tsx`** - Removed `projectId` from `CreatePRDialog.show()` call ### Benefits - More efficient - fetches only branches for the specific repo needed instead of all project repos - Better caching - React Query caches branches per repo for efficient reuse - Cleaner code - removed manual filtering logic from components - Type safety - hooks return `GitBranch[]` directly instead of `RepositoryBranches[]` * All tasks complete. Here's a summary of the changes made: ## Summary Successfully decoupled the branches API from project IDs. ### Changes Made **1. `frontend/src/hooks/useBranches.ts`** - Refactored to accept `repos: Array<{ id: string; name: string }>` instead of `projectId` - Now uses `useQueries` to fetch branches for multiple repos in parallel - Reuses the same query keys as `useRepoBranches` for cache consistency **2. `frontend/src/hooks/useRepoBranchSelection.ts`** - Refactored to accept `repos: Repo[]` instead of `projectId` - Removed internal fetch of project repos (caller now provides them) - Removed `projectRepos` from return value - Uses refactored `useBranches(repos)` **3. `frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx`** - Added `useQuery` to fetch project repos separately - Passes `repos` to `useRepoBranchSelection` **4. `frontend/src/components/dialogs/tasks/TaskFormDialog.tsx`** - Already fetched `projectRepos` - Updated to pass `projectRepos` to refactored `useBranches` **5. `frontend/src/components/layout/Navbar.tsx`** - Replaced `useBranches(projectId)` with `useQuery` for `projectsApi.getRepositories` - Only needed repo count, not branches **6. `frontend/src/components/projects/ProjectCard.tsx`** - Replaced `useBranches(project.id)` with `useQuery` for `projectsApi.getRepositories` - Only needed repo count, not branches ### Benefits - Branch hooks no longer require a project ID - Repos can come from any source (project, attempt, or future sources without projects) - Better separation of concerns - Enables future task attempts without projects attached * Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a * The fix is already applied. Type checks pass with `Repo[]` instead of the loose `Array<{ id: string; name: string }>` type. This makes it explicit that `useBranches` expects actual `Repo` objects (with `Repo.id` being the repository's UUID), not project-repo junction records. * All done. The refactor is complete: **Summary of changes:** 1. **Created `useProjectRepos` hook** (`frontend/src/hooks/useProjectRepos.ts`) - A reusable hook that fetches project repositories with proper query key management 2. **Updated 5 files** to use the new hook instead of duplicating the `useQuery` pattern: - `CreateAttemptDialog.tsx` - `TaskFormDialog.tsx` - `Navbar.tsx` - `ProjectCard.tsx` - `NoServerContent.tsx` 3. **Earlier refactors** (from before the summary): - `useBranches.ts` - Now accepts `repos: Repo[]` instead of `projectId` - `useRepoBranchSelection.ts` - Now accepts `repos: Repo[]` instead of `projectId` This enables future task attempts without projects attached, as branch fetching is now decoupled from project IDs. * No more usages. The branches in `ProjectTasks.tsx` are used for the diffs panel - showing available branches for git operations when viewing an attempt's diffs. * Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a * All done. The refactor is complete: **Summary of changes:** `GitOperations` now fetches its own branches using `useRepoBranches(selectedRepoId)` instead of receiving them as a prop. This: 1. Removes prop drilling through `ProjectTasks` → `DiffsPanelContainer` → `DiffsPanel` → `GitOperations` 2. Removes the need for `ProjectTasks` to fetch all project repos just to get branches for the selected repo 3. Makes `GitOperations` self-contained - it already has `selectedRepoId` from `useAttemptRepo`, so it can fetch its own branches 4. Simplifies `GitActionsDialog` which no longer needs to fetch branches either * All done. Here's a summary of the changes: **Removed `projectId` from GitOperations flow:** 1. **`useRebase.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)` 2. **`useChangeTargetBranch.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)` 3. **`useMerge.ts`** - Updated invalidation to use `repoBranchKeys.all` instead of `['projectBranches']` 4. **`useGitOperations.ts`** - Changed param from `projectId` to `repoId` 5. **`GitOperations.tsx`** - Removed `projectId` prop, now passes `selectedRepoId` to `useGitOperations` 6. **`GitActionsDialog.tsx`** - Removed `projectId` from props and internal logic 7. **`DiffsPanelContainer`** (in ProjectTasks.tsx) - Removed `projectId` prop 8. **Callers of `GitActionsDialog.show`** - Removed `projectId` from `NextActionCard.tsx` and `actions-dropdown.tsx` * The cleanup is complete. Here's a summary of what was done: **Removed dead project branches code:** 1. **Backend** (`crates/server/src/routes/projects.rs`): - Removed `get_project_branches` handler and route - Removed `RepositoryBranches` and `ProjectBranchesResponse` structs - Fixed unused `Serialize` import warning 2. **Type Generation** (`crates/server/src/bin/generate_types.rs`): - Removed exports for `ProjectBranchesResponse` and `RepositoryBranches` 3. **Frontend Hooks** (`frontend/src/hooks/useBranches.ts`): - Defined local `RepositoryBranches` type instead of importing from shared/types 4. **Frontend API** (`frontend/src/lib/api.ts`): - Removed `projectsApi.getBranches` method - Removed `RepositoryBranches` and `ProjectBranchesResponse` imports Both TypeScript and Rust compilation pass without errors. * Done. Removed the `RepositoryBranches` type and simplified `useBranches` to return `Map`. Changes: 1. **`useBranches.ts`** - Returns `Map` instead of `RepositoryBranches[]` 2. **`useRepoBranchSelection.ts`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()` 3. **`TaskFormDialog.tsx`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()` Type checks pass. * Done. Deleted `useBranches` entirely and consolidated everything into `useRepoBranchSelection`. Summary: 1. **Deleted** `frontend/src/hooks/useBranches.ts` 2. **Updated** `useRepoBranchSelection.ts` - inlined the `useQueries` logic directly 3. **Updated** `hooks/index.ts` - removed `useBranches` export 4. **Updated** `TaskFormDialog.tsx` - now uses `useRepoBranchSelection` instead of duplicating the config-building logic Type checks pass. * types --- crates/server/src/bin/generate_types.rs | 2 - crates/server/src/routes/projects.rs | 46 +------------- .../NormalizedConversation/NextActionCard.tsx | 3 +- .../dialogs/tasks/CreateAttemptDialog.tsx | 10 ++- .../dialogs/tasks/CreatePRDialog.tsx | 63 +++++++------------ .../dialogs/tasks/GitActionsDialog.tsx | 53 ++-------------- .../dialogs/tasks/TaskFormDialog.tsx | 45 +++---------- frontend/src/components/layout/Navbar.tsx | 4 +- .../src/components/projects/ProjectCard.tsx | 5 +- .../TaskDetails/preview/NoServerContent.tsx | 12 +--- .../tasks/Toolbar/GitOperations.tsx | 10 +-- .../src/components/ui/actions-dropdown.tsx | 1 - frontend/src/hooks/index.ts | 3 +- frontend/src/hooks/useBranches.ts | 25 -------- frontend/src/hooks/useChangeTargetBranch.ts | 7 ++- frontend/src/hooks/useGitOperations.ts | 6 +- frontend/src/hooks/useMerge.ts | 5 +- frontend/src/hooks/useProjectRepos.ts | 17 +++++ frontend/src/hooks/useRebase.ts | 9 +-- frontend/src/hooks/useRepoBranchSelection.ts | 44 ++++++------- frontend/src/hooks/useRepoBranches.ts | 24 +++++++ frontend/src/lib/api.ts | 8 --- frontend/src/pages/ProjectTasks.tsx | 34 +--------- .../src/pages/settings/ProjectSettings.tsx | 6 +- shared/types.ts | 4 -- 25 files changed, 137 insertions(+), 309 deletions(-) delete mode 100644 frontend/src/hooks/useBranches.ts create mode 100644 frontend/src/hooks/useProjectRepos.ts create mode 100644 frontend/src/hooks/useRepoBranches.ts diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index e4a67221..f1212bb4 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -103,8 +103,6 @@ fn generate_types_content() -> String { server::routes::config::CheckEditorAvailabilityResponse::decl(), server::routes::config::CheckAgentAvailabilityQuery::decl(), server::routes::oauth::CurrentUserResponse::decl(), - server::routes::projects::RepositoryBranches::decl(), - server::routes::projects::ProjectBranchesResponse::decl(), server::routes::task_attempts::CreateFollowUpAttempt::decl(), server::routes::task_attempts::ChangeTargetBranchRequest::decl(), server::routes::task_attempts::ChangeTargetBranchResponse::decl(), diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs index 6a3563cf..925f641a 100644 --- a/crates/server/src/routes/projects.rs +++ b/crates/server/src/routes/projects.rs @@ -14,9 +14,9 @@ use db::models::{ repo::Repo, }; use deployment::Deployment; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use services::services::{ - file_search_cache::SearchQuery, git::GitBranch, project::ProjectServiceError, + file_search_cache::SearchQuery, project::ProjectServiceError, remote_client::CreateRemoteProjectPayload, }; use ts_rs::TS; @@ -28,20 +28,6 @@ use uuid::Uuid; use crate::{DeploymentImpl, error::ApiError, middleware::load_project_middleware}; -/// Branches for a single repository -#[derive(Debug, Serialize, TS)] -pub struct RepositoryBranches { - pub repository_id: Uuid, - pub repository_name: String, - pub branches: Vec, -} - -/// Response containing branches grouped by repository -#[derive(Debug, Serialize, TS)] -pub struct ProjectBranchesResponse { - pub repositories: Vec, -} - #[derive(Deserialize, TS)] pub struct LinkToExistingRequest { pub remote_project_id: Uuid, @@ -66,33 +52,6 @@ pub async fn get_project( Ok(ResponseJson(ApiResponse::success(project))) } -pub async fn get_project_branches( - Extension(project): Extension, - State(deployment): State, -) -> Result>, ApiError> { - let repositories = deployment - .project() - .get_repositories(&deployment.db().pool, project.id) - .await?; - - let mut repo_branches = Vec::with_capacity(repositories.len()); - - for repo in repositories { - let branches = deployment.git().get_all_branches(&repo.path)?; - repo_branches.push(RepositoryBranches { - repository_id: repo.id, - repository_name: repo.name, - branches, - }); - } - - Ok(ResponseJson(ApiResponse::success( - ProjectBranchesResponse { - repositories: repo_branches, - }, - ))) -} - pub async fn link_project_to_existing_remote( Extension(project): Extension, State(deployment): State, @@ -583,7 +542,6 @@ pub fn router(deployment: &DeploymentImpl) -> Router { get(get_project).put(update_project).delete(delete_project), ) .route("/remote/members", get(get_project_remote_members)) - .route("/branches", get(get_project_branches)) .route("/search", get(search_project_files)) .route("/open-editor", post(open_project_in_editor)) .route( diff --git a/frontend/src/components/NormalizedConversation/NextActionCard.tsx b/frontend/src/components/NormalizedConversation/NextActionCard.tsx index 4d1821f9..e2963b0e 100644 --- a/frontend/src/components/NormalizedConversation/NextActionCard.tsx +++ b/frontend/src/components/NormalizedConversation/NextActionCard.tsx @@ -122,9 +122,8 @@ export function NextActionCard({ GitActionsDialog.show({ attemptId, task, - projectId: project?.id, }); - }, [attemptId, task, project?.id]); + }, [attemptId, task]); const handleRunSetup = useCallback(async () => { if (!attemptId || !attempt) return; diff --git a/frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx b/frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx index 56b27d6b..e0e3783a 100644 --- a/frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx +++ b/frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx @@ -18,6 +18,7 @@ import { useAttempt, useRepoBranchSelection, useTaskAttempts, + useProjectRepos, } from '@/hooks'; import { useProject } from '@/contexts/ProjectContext'; import { useUserSystem } from '@/components/ConfigProvider'; @@ -66,17 +67,19 @@ const CreateAttemptDialogImpl = NiceModal.create( { enabled: modal.visible && !!parentAttemptId } ); + const { data: projectRepos = [], isLoading: isLoadingRepos } = + useProjectRepos(projectId, { enabled: modal.visible }); + const { configs: repoBranchConfigs, - projectRepos, isLoading: isLoadingBranches, setRepoBranch, getAttemptRepoInputs, reset: resetBranchSelection, } = useRepoBranchSelection({ - projectId, + repos: projectRepos, initialBranch: parentAttempt?.branch, - enabled: modal.visible, + enabled: modal.visible && projectRepos.length > 0, }); const latestAttempt = useMemo(() => { @@ -118,6 +121,7 @@ const CreateAttemptDialogImpl = NiceModal.create( const effectiveProfile = userSelectedProfile ?? defaultProfile; const isLoadingInitial = + isLoadingRepos || isLoadingBranches || isLoadingAttempts || isLoadingTask || diff --git a/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx b/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx index 1cc937de..2e5a4c26 100644 --- a/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx +++ b/frontend/src/components/dialogs/tasks/CreatePRDialog.tsx @@ -17,16 +17,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { attemptsApi } from '@/lib/api.ts'; import { useTranslation } from 'react-i18next'; -import { - GitBranch, - RepositoryBranches, - TaskAttempt, - TaskWithAttemptStatus, -} from 'shared/types'; -import { projectsApi } from '@/lib/api.ts'; +import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types'; import { Loader2 } from 'lucide-react'; import NiceModal, { useModal } from '@ebay/nice-modal-react'; -import { useAuth } from '@/hooks'; +import { useAuth, useRepoBranches } from '@/hooks'; import { GhCliHelpInstructions, GhCliSetupDialog, @@ -43,12 +37,11 @@ import { defineModal } from '@/lib/modals'; interface CreatePRDialogProps { attempt: TaskAttempt; task: TaskWithAttemptStatus; - projectId: string; repoId: string; } const CreatePRDialogImpl = NiceModal.create( - ({ attempt, task, projectId, repoId }) => { + ({ attempt, task, repoId }) => { const modal = useModal(); const { t } = useTranslation('tasks'); const { isLoaded } = useAuth(); @@ -61,18 +54,22 @@ const CreatePRDialogImpl = NiceModal.create( const [ghCliHelp, setGhCliHelp] = useState( null ); - const [branches, setBranches] = useState([]); - const [branchesLoading, setBranchesLoading] = useState(false); const [isDraft, setIsDraft] = useState(false); const [autoGenerateDescription, setAutoGenerateDescription] = useState( config?.pr_auto_description_enabled ?? false ); + const { data: branches = [], isLoading: branchesLoading } = useRepoBranches( + repoId, + { enabled: modal.visible && !!repoId } + ); + const getGhCliHelpTitle = (variant: GhCliSupportVariant) => variant === 'homebrew' ? 'Homebrew is required for automatic setup' : 'GitHub CLI needs manual setup'; + // Initialize form when dialog opens useEffect(() => { if (!modal.visible || !isLoaded) { return; @@ -80,32 +77,19 @@ const CreatePRDialogImpl = NiceModal.create( setPrTitle(`${task.title} (vibe-kanban)`); setPrBody(task.description || ''); - - // Always fetch branches for dropdown population - if (projectId) { - setBranchesLoading(true); - projectsApi - .getBranches(projectId) - .then((repoBranches: RepositoryBranches[]) => { - const repoData = repoBranches.find( - (r) => r.repository_id === repoId - ); - const branchesForRepo = repoData?.branches ?? []; - setBranches(branchesForRepo); - - // Set smart default: current branch (target_branch is now per-repo in AttemptRepo) - const currentBranch = branchesForRepo.find((b) => b.is_current); - if (currentBranch) { - setPrBaseBranch(currentBranch.name); - } - }) - .catch(console.error) - .finally(() => setBranchesLoading(false)); - } - - setError(null); // Reset error when opening + setError(null); setGhCliHelp(null); - }, [modal.visible, isLoaded, task, attempt, projectId, repoId]); + }, [modal.visible, isLoaded, task]); + + // Set default base branch when branches are loaded + useEffect(() => { + if (branches.length > 0 && !prBaseBranch) { + const currentBranch = branches.find((b) => b.is_current); + if (currentBranch) { + setPrBaseBranch(currentBranch.name); + } + } + }, [branches, prBaseBranch]); const isMacEnvironment = useMemo( () => environment?.os_type?.toLowerCase().includes('mac'), @@ -113,7 +97,7 @@ const CreatePRDialogImpl = NiceModal.create( ); const handleConfirmCreatePR = useCallback(async () => { - if (!projectId || !attempt.id) return; + if (!repoId || !attempt.id) return; setError(null); setGhCliHelp(null); @@ -227,7 +211,7 @@ const CreatePRDialogImpl = NiceModal.create( } }, [ attempt, - projectId, + repoId, prBaseBranch, prBody, prTitle, @@ -237,7 +221,6 @@ const CreatePRDialogImpl = NiceModal.create( modal, isMacEnvironment, t, - repoId, ]); const handleCancelCreatePR = useCallback(() => { diff --git a/frontend/src/components/dialogs/tasks/GitActionsDialog.tsx b/frontend/src/components/dialogs/tasks/GitActionsDialog.tsx index f9d45961..bfa7e8ab 100644 --- a/frontend/src/components/dialogs/tasks/GitActionsDialog.tsx +++ b/frontend/src/components/dialogs/tasks/GitActionsDialog.tsx @@ -1,4 +1,3 @@ -import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ExternalLink, GitPullRequest } from 'lucide-react'; import { @@ -12,41 +11,28 @@ import GitOperations from '@/components/tasks/Toolbar/GitOperations'; import { useTaskAttempt } from '@/hooks/useTaskAttempt'; import { useBranchStatus, useAttemptExecution } from '@/hooks'; import { useAttemptRepo } from '@/hooks/useAttemptRepo'; -import { useProject } from '@/contexts/ProjectContext'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; import { GitOperationsProvider, useGitOperationsError, } from '@/contexts/GitOperationsContext'; -import { projectsApi } from '@/lib/api'; -import type { - GitBranch, - Merge, - RepositoryBranches, - TaskAttempt, - TaskWithAttemptStatus, -} from 'shared/types'; +import type { Merge, TaskAttempt, TaskWithAttemptStatus } from 'shared/types'; import NiceModal, { useModal } from '@ebay/nice-modal-react'; import { defineModal } from '@/lib/modals'; export interface GitActionsDialogProps { attemptId: string; task?: TaskWithAttemptStatus; - projectId?: string; } interface GitActionsDialogContentProps { attempt: TaskAttempt; task: TaskWithAttemptStatus; - projectId: string; - branches: GitBranch[]; } function GitActionsDialogContent({ attempt, task, - projectId, - branches, }: GitActionsDialogContentProps) { const { t } = useTranslation('tasks'); const { data: branchStatus } = useBranchStatus(attempt.id); @@ -96,9 +82,7 @@ function GitActionsDialogContent({ ( - ({ attemptId, task, projectId: providedProjectId }) => { + ({ attemptId, task }) => { const modal = useModal(); const { t } = useTranslation('tasks'); - const { project } = useProject(); - const effectiveProjectId = providedProjectId ?? project?.id; const { data: attempt } = useTaskAttempt(attemptId); - const { selectedRepoId } = useAttemptRepo(attemptId); - - const [repoBranches, setRepoBranches] = useState([]); - const [loadingBranches, setLoadingBranches] = useState(true); - - useEffect(() => { - if (!effectiveProjectId) return; - setLoadingBranches(true); - projectsApi - .getBranches(effectiveProjectId) - .then(setRepoBranches) - .catch(() => setRepoBranches([])) - .finally(() => setLoadingBranches(false)); - }, [effectiveProjectId]); - - const branches = useMemo( - () => - repoBranches.find((r) => r.repository_id === selectedRepoId) - ?.branches ?? [], - [repoBranches, selectedRepoId] - ); const handleOpenChange = (open: boolean) => { if (!open) { @@ -143,8 +104,7 @@ const GitActionsDialogImpl = NiceModal.create( } }; - const isLoading = - !attempt || !effectiveProjectId || loadingBranches || !task; + const isLoading = !attempt || !task; return ( @@ -163,12 +123,7 @@ const GitActionsDialogImpl = NiceModal.create( key={attempt.id} attemptId={attempt.id} > - + )} diff --git a/frontend/src/components/dialogs/tasks/TaskFormDialog.tsx b/frontend/src/components/dialogs/tasks/TaskFormDialog.tsx index c83bc2fe..85852bf6 100644 --- a/frontend/src/components/dialogs/tasks/TaskFormDialog.tsx +++ b/frontend/src/components/dialogs/tasks/TaskFormDialog.tsx @@ -31,11 +31,11 @@ import RepoBranchSelector from '@/components/tasks/RepoBranchSelector'; import { ExecutorProfileSelector } from '@/components/settings'; import { useUserSystem } from '@/components/ConfigProvider'; import { - useBranches, useTaskImages, useImageUpload, useTaskMutations, - type RepoBranchConfig, + useProjectRepos, + useRepoBranchSelection, } from '@/hooks'; import { useKeySubmitTask, @@ -50,8 +50,6 @@ import type { ExecutorProfileId, ImageResponse, } from 'shared/types'; -import { projectsApi } from '@/lib/api'; -import { useQuery } from '@tanstack/react-query'; interface Task { id: string; @@ -104,43 +102,20 @@ const TaskFormDialogImpl = NiceModal.create((props) => { const [showDiscardWarning, setShowDiscardWarning] = useState(false); const forceCreateOnlyRef = useRef(false); - const { data: repoBranches, isLoading: branchesLoading } = - useBranches(projectId); const { data: taskImages } = useTaskImages( editMode ? props.task.id : undefined ); - const { data: projectRepos = [] } = useQuery({ - queryKey: ['projectRepositories', projectId], - queryFn: () => projectsApi.getRepositories(projectId), + const { data: projectRepos = [] } = useProjectRepos(projectId, { enabled: modal.visible, }); - - const repoBranchConfigs = useMemo((): RepoBranchConfig[] => { - return projectRepos.map((repo) => { - const repoBranchData = repoBranches?.find( - (rb) => rb.repository_id === repo.id - ); - const branches = repoBranchData?.branches ?? []; - - let targetBranch: string | null = null; - const initialBranch = - mode === 'subtask' ? props.initialBaseBranch : undefined; - - if (initialBranch && branches.some((b) => b.name === initialBranch)) { - targetBranch = initialBranch; - } else { - const currentBranch = branches.find((b) => b.is_current); - targetBranch = currentBranch?.name ?? branches[0]?.name ?? null; - } - - return { - repoId: repo.id, - repoDisplayName: repo.display_name, - targetBranch, - branches, - }; + const initialBranch = + mode === 'subtask' ? props.initialBaseBranch : undefined; + const { configs: repoBranchConfigs, isLoading: branchesLoading } = + useRepoBranchSelection({ + repos: projectRepos, + initialBranch, + enabled: modal.visible && projectRepos.length > 0, }); - }, [projectRepos, repoBranches, mode, props]); const defaultRepoBranches = useMemo((): RepoBranch[] => { return repoBranchConfigs diff --git a/frontend/src/components/layout/Navbar.tsx b/frontend/src/components/layout/Navbar.tsx index fdfd183d..9fd144ce 100644 --- a/frontend/src/components/layout/Navbar.tsx +++ b/frontend/src/components/layout/Navbar.tsx @@ -27,7 +27,7 @@ import { openTaskForm } from '@/lib/openTaskForm'; import { useProject } from '@/contexts/ProjectContext'; import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor'; import { OpenInIdeButton } from '@/components/ide/OpenInIdeButton'; -import { useBranches } from '@/hooks/useBranches'; +import { useProjectRepos } from '@/hooks'; import { useDiscordOnlineCount } from '@/hooks/useDiscordOnlineCount'; import { useTranslation } from 'react-i18next'; import { Switch } from '@/components/ui/switch'; @@ -80,7 +80,7 @@ export function Navbar() { const { data: onlineCount } = useDiscordOnlineCount(); const { loginStatus, reloadSystem } = useUserSystem(); - const { data: repos } = useBranches(projectId); + const { data: repos } = useProjectRepos(projectId); const isSingleRepoProject = repos?.length === 1; const setSearchBarRef = useCallback( diff --git a/frontend/src/components/projects/ProjectCard.tsx b/frontend/src/components/projects/ProjectCard.tsx index 82ca0417..492b7b1f 100644 --- a/frontend/src/components/projects/ProjectCard.tsx +++ b/frontend/src/components/projects/ProjectCard.tsx @@ -24,12 +24,11 @@ import { import { Project } from 'shared/types'; import { useEffect, useRef } from 'react'; import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor'; -import { useNavigateWithSearch } from '@/hooks'; +import { useNavigateWithSearch, useProjectRepos } from '@/hooks'; import { projectsApi } from '@/lib/api'; import { LinkProjectDialog } from '@/components/dialogs/projects/LinkProjectDialog'; import { useTranslation } from 'react-i18next'; import { useProjectMutations } from '@/hooks/useProjectMutations'; -import { useBranches } from '@/hooks/useBranches'; type Props = { project: Project; @@ -51,7 +50,7 @@ function ProjectCard({ const handleOpenInEditor = useOpenProjectInEditor(project); const { t } = useTranslation('projects'); - const { data: repos } = useBranches(project.id); + const { data: repos } = useProjectRepos(project.id); const isSingleRepoProject = repos?.length === 1; const { unlinkProject } = useProjectMutations({ diff --git a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx index f2dda4cd..720ce87f 100644 --- a/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx +++ b/frontend/src/components/tasks/TaskDetails/preview/NoServerContent.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useQuery } from '@tanstack/react-query'; import { Play, Edit3, @@ -21,7 +20,7 @@ import { import { useUserSystem } from '@/components/ConfigProvider'; import { useTaskMutations } from '@/hooks/useTaskMutations'; import { useProjectMutations } from '@/hooks/useProjectMutations'; -import { projectsApi } from '@/lib/api'; +import { useProjectRepos } from '@/hooks'; import { COMPANION_INSTALL_TASK_TITLE, COMPANION_INSTALL_TASK_DESCRIPTION, @@ -53,14 +52,7 @@ export function NoServerContent({ const { createAndStart } = useTaskMutations(project?.id); const { updateProject } = useProjectMutations(); - const { data: projectRepos = [] } = useQuery({ - queryKey: ['projectRepositories', project?.id], - queryFn: () => - project?.id - ? projectsApi.getRepositories(project.id) - : Promise.resolve([]), - enabled: !!project?.id, - }); + const { data: projectRepos = [] } = useProjectRepos(project?.id); // Create strategy-based placeholders const placeholders = system.environment diff --git a/frontend/src/components/tasks/Toolbar/GitOperations.tsx b/frontend/src/components/tasks/Toolbar/GitOperations.tsx index 2b816a3f..fb3aa03e 100644 --- a/frontend/src/components/tasks/Toolbar/GitOperations.tsx +++ b/frontend/src/components/tasks/Toolbar/GitOperations.tsx @@ -19,7 +19,6 @@ import { useCallback, useMemo, useState } from 'react'; import type { RepoBranchStatus, Merge, - GitBranch, TaskAttempt, TaskWithAttemptStatus, } from 'shared/types'; @@ -30,13 +29,12 @@ import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog'; import { useTranslation } from 'react-i18next'; import { useAttemptRepo } from '@/hooks/useAttemptRepo'; import { useGitOperations } from '@/hooks/useGitOperations'; +import { useRepoBranches } from '@/hooks'; interface GitOperationsProps { selectedAttempt: TaskAttempt; task: TaskWithAttemptStatus; - projectId: string; branchStatus: RepoBranchStatus[] | null; - branches: GitBranch[]; isAttemptRunning: boolean; selectedBranch: string | null; layout?: 'horizontal' | 'vertical'; @@ -47,19 +45,18 @@ export type GitOperationsInputs = Omit; function GitOperations({ selectedAttempt, task, - projectId, branchStatus, - branches, isAttemptRunning, selectedBranch, layout = 'horizontal', }: GitOperationsProps) { const { t } = useTranslation('tasks'); - const git = useGitOperations(selectedAttempt.id, projectId); const { repos, selectedRepoId, setSelectedRepoId } = useAttemptRepo( selectedAttempt.id ); + const git = useGitOperations(selectedAttempt.id, selectedRepoId ?? undefined); + const { data: branches = [] } = useRepoBranches(selectedRepoId); const isChangingTargetBranch = git.states.changeTargetBranchPending; // Local state for git operations @@ -256,7 +253,6 @@ function GitOperations({ CreatePRDialog.show({ attempt: selectedAttempt, task, - projectId, repoId: getSelectedRepoId(), }); }; diff --git a/frontend/src/components/ui/actions-dropdown.tsx b/frontend/src/components/ui/actions-dropdown.tsx index 18a82c81..aeabf986 100644 --- a/frontend/src/components/ui/actions-dropdown.tsx +++ b/frontend/src/components/ui/actions-dropdown.tsx @@ -128,7 +128,6 @@ export function ActionsDropdown({ GitActionsDialog.show({ attemptId: attempt.id, task, - projectId, }); }; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index c350155c..ff982709 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -16,7 +16,8 @@ export { useNavigateWithSearch } from './useNavigateWithSearch'; export { useGitOperations } from './useGitOperations'; export { useTask } from './useTask'; export { useAttempt } from './useAttempt'; -export { useBranches } from './useBranches'; +export { useRepoBranches } from './useRepoBranches'; +export { useProjectRepos } from './useProjectRepos'; export { useRepoBranchSelection } from './useRepoBranchSelection'; export type { RepoBranchConfig } from './useRepoBranchSelection'; export { useTaskAttempts } from './useTaskAttempts'; diff --git a/frontend/src/hooks/useBranches.ts b/frontend/src/hooks/useBranches.ts deleted file mode 100644 index 6ee1f7fe..00000000 --- a/frontend/src/hooks/useBranches.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { projectsApi } from '@/lib/api'; -import type { RepositoryBranches } from 'shared/types'; - -export const branchKeys = { - all: ['projectBranches'] as const, - byProject: (projectId: string | undefined) => - ['projectBranches', projectId] as const, -}; - -type Options = { - enabled?: boolean; -}; - -export function useBranches(projectId?: string, opts?: Options) { - const enabled = (opts?.enabled ?? true) && !!projectId; - - return useQuery({ - queryKey: branchKeys.byProject(projectId), - queryFn: () => projectsApi.getBranches(projectId!), - enabled, - staleTime: 60_000, - refetchOnWindowFocus: true, - }); -} diff --git a/frontend/src/hooks/useChangeTargetBranch.ts b/frontend/src/hooks/useChangeTargetBranch.ts index 7529c74f..f7b33803 100644 --- a/frontend/src/hooks/useChangeTargetBranch.ts +++ b/frontend/src/hooks/useChangeTargetBranch.ts @@ -4,6 +4,7 @@ import type { ChangeTargetBranchRequest, ChangeTargetBranchResponse, } from 'shared/types'; +import { repoBranchKeys } from './useRepoBranches'; type ChangeTargetBranchParams = { newTargetBranch: string; @@ -12,7 +13,7 @@ type ChangeTargetBranchParams = { export function useChangeTargetBranch( attemptId: string | undefined, - projectId: string | undefined, + repoId: string | undefined, onSuccess?: (data: ChangeTargetBranchResponse) => void, onError?: (err: unknown) => void ) { @@ -45,9 +46,9 @@ export function useChangeTargetBranch( }); } - if (projectId) { + if (repoId) { queryClient.invalidateQueries({ - queryKey: ['projectBranches', projectId], + queryKey: repoBranchKeys.byRepo(repoId), }); } diff --git a/frontend/src/hooks/useGitOperations.ts b/frontend/src/hooks/useGitOperations.ts index 1e6d598f..2a7b5a2c 100644 --- a/frontend/src/hooks/useGitOperations.ts +++ b/frontend/src/hooks/useGitOperations.ts @@ -10,13 +10,13 @@ import { ForcePushDialog } from '@/components/dialogs/git/ForcePushDialog'; export function useGitOperations( attemptId: string | undefined, - projectId: string | undefined + repoId: string | undefined ) { const { setError } = useGitOperationsError(); const rebase = useRebase( attemptId, - projectId, + repoId, () => setError(null), (err: Result) => { if (!err.success) { @@ -78,7 +78,7 @@ export function useGitOperations( const changeTargetBranch = useChangeTargetBranch( attemptId, - projectId, + repoId, () => setError(null), (err: unknown) => { const message = diff --git a/frontend/src/hooks/useMerge.ts b/frontend/src/hooks/useMerge.ts index be781056..72d60bad 100644 --- a/frontend/src/hooks/useMerge.ts +++ b/frontend/src/hooks/useMerge.ts @@ -1,5 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { attemptsApi } from '@/lib/api'; +import { repoBranchKeys } from './useRepoBranches'; type MergeParams = { repoId: string; @@ -23,8 +24,8 @@ export function useMerge( // Refresh attempt-specific branch information queryClient.invalidateQueries({ queryKey: ['branchStatus', attemptId] }); - // Invalidate all project branches queries - queryClient.invalidateQueries({ queryKey: ['projectBranches'] }); + // Invalidate all repo branches queries + queryClient.invalidateQueries({ queryKey: repoBranchKeys.all }); onSuccess?.(); }, diff --git a/frontend/src/hooks/useProjectRepos.ts b/frontend/src/hooks/useProjectRepos.ts new file mode 100644 index 00000000..e8ca8feb --- /dev/null +++ b/frontend/src/hooks/useProjectRepos.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { projectsApi } from '@/lib/api'; +import type { Repo } from 'shared/types'; + +type Options = { + enabled?: boolean; +}; + +export function useProjectRepos(projectId?: string, opts?: Options) { + const enabled = (opts?.enabled ?? true) && !!projectId; + + return useQuery({ + queryKey: ['projectRepositories', projectId], + queryFn: () => projectsApi.getRepositories(projectId!), + enabled, + }); +} diff --git a/frontend/src/hooks/useRebase.ts b/frontend/src/hooks/useRebase.ts index 87aea020..952653f9 100644 --- a/frontend/src/hooks/useRebase.ts +++ b/frontend/src/hooks/useRebase.ts @@ -2,10 +2,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { attemptsApi, Result } from '@/lib/api'; import type { RebaseTaskAttemptRequest } from 'shared/types'; import type { GitOperationError } from 'shared/types'; +import { repoBranchKeys } from './useRepoBranches'; export function useRebase( attemptId: string | undefined, - projectId: string | undefined, + repoId: string | undefined, onSuccess?: () => void, onError?: (err: Result) => void ) { @@ -47,10 +48,10 @@ export function useRebase( queryKey: ['taskAttempt', attemptId], }); - // Refresh branch list used by PR dialog - if (projectId) { + // Refresh branch list + if (repoId) { queryClient.invalidateQueries({ - queryKey: ['projectBranches', projectId], + queryKey: repoBranchKeys.byRepo(repoId), }); } diff --git a/frontend/src/hooks/useRepoBranchSelection.ts b/frontend/src/hooks/useRepoBranchSelection.ts index a4dfb149..fa23d87f 100644 --- a/frontend/src/hooks/useRepoBranchSelection.ts +++ b/frontend/src/hooks/useRepoBranchSelection.ts @@ -1,8 +1,8 @@ import { useState, useMemo, useCallback } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useBranches } from './useBranches'; -import { projectsApi } from '@/lib/api'; -import type { GitBranch, Repo, RepositoryBranches } from 'shared/types'; +import { useQueries } from '@tanstack/react-query'; +import { repoApi } from '@/lib/api'; +import { repoBranchKeys } from './useRepoBranches'; +import type { GitBranch, Repo } from 'shared/types'; export type RepoBranchConfig = { repoId: string; @@ -12,15 +12,13 @@ export type RepoBranchConfig = { }; type UseRepoBranchSelectionOptions = { - projectId: string | undefined; + repos: Repo[]; initialBranch?: string | null; enabled?: boolean; }; type UseRepoBranchSelectionReturn = { configs: RepoBranchConfig[]; - repositoryBranches: RepositoryBranches[]; - projectRepos: Repo[]; isLoading: boolean; setRepoBranch: (repoId: string, branch: string) => void; getAttemptRepoInputs: () => Array<{ repo_id: string; target_branch: string }>; @@ -28,7 +26,7 @@ type UseRepoBranchSelectionReturn = { }; export function useRepoBranchSelection({ - projectId, + repos, initialBranch, enabled = true, }: UseRepoBranchSelectionOptions): UseRepoBranchSelectionReturn { @@ -36,22 +34,20 @@ export function useRepoBranchSelection({ Record >({}); - const { data: repositoryBranches = [], isLoading: isLoadingBranches } = - useBranches(projectId, { enabled: enabled && !!projectId }); - - const { data: projectRepos = [], isLoading: isLoadingRepos } = useQuery({ - queryKey: ['projectRepositories', projectId], - queryFn: () => - projectId ? projectsApi.getRepositories(projectId) : Promise.resolve([]), - enabled: enabled && !!projectId, + const queries = useQueries({ + queries: repos.map((repo) => ({ + queryKey: repoBranchKeys.byRepo(repo.id), + queryFn: () => repoApi.getBranches(repo.id), + enabled, + staleTime: 60_000, + })), }); + const isLoadingBranches = queries.some((q) => q.isLoading); + const configs = useMemo((): RepoBranchConfig[] => { - return projectRepos.map((repo) => { - const repoBranchData = repositoryBranches.find( - (rb) => rb.repository_id === repo.id - ); - const branches = repoBranchData?.branches ?? []; + return repos.map((repo, i) => { + const branches = queries[i]?.data ?? []; let targetBranch: string | null = userOverrides[repo.id] ?? null; @@ -71,7 +67,7 @@ export function useRepoBranchSelection({ branches, }; }); - }, [projectRepos, repositoryBranches, userOverrides, initialBranch]); + }, [repos, queries, userOverrides, initialBranch]); const setRepoBranch = useCallback((repoId: string, branch: string) => { setUserOverrides((prev) => ({ @@ -95,9 +91,7 @@ export function useRepoBranchSelection({ return { configs, - repositoryBranches, - projectRepos, - isLoading: isLoadingBranches || isLoadingRepos, + isLoading: isLoadingBranches, setRepoBranch, getAttemptRepoInputs, reset, diff --git a/frontend/src/hooks/useRepoBranches.ts b/frontend/src/hooks/useRepoBranches.ts new file mode 100644 index 00000000..4e522f81 --- /dev/null +++ b/frontend/src/hooks/useRepoBranches.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { repoApi } from '@/lib/api'; +import type { GitBranch } from 'shared/types'; + +export const repoBranchKeys = { + all: ['repoBranches'] as const, + byRepo: (repoId: string | undefined) => ['repoBranches', repoId] as const, +}; + +type Options = { + enabled?: boolean; +}; + +export function useRepoBranches(repoId?: string | null, opts?: Options) { + const enabled = (opts?.enabled ?? true) && !!repoId; + + return useQuery({ + queryKey: repoBranchKeys.byRepo(repoId ?? undefined), + queryFn: () => repoApi.getBranches(repoId!), + enabled, + staleTime: 60_000, + refetchOnWindowFocus: true, + }); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0dab0b28..f462775c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -20,8 +20,6 @@ import { ProjectRepo, Repo, RepoWithTargetBranch, - RepositoryBranches, - ProjectBranchesResponse, CreateProject, CreateProjectRepo, UpdateProjectRepo, @@ -288,12 +286,6 @@ export const projectsApi = { return handleApiResponse(response); }, - getBranches: async (id: string): Promise => { - const response = await makeRequest(`/api/projects/${id}/branches`); - const data = await handleApiResponse(response); - return data.repositories; - }, - searchFiles: async ( id: string, query: string, diff --git a/frontend/src/pages/ProjectTasks.tsx b/frontend/src/pages/ProjectTasks.tsx index d0562a8c..090b31af 100644 --- a/frontend/src/pages/ProjectTasks.tsx +++ b/frontend/src/pages/ProjectTasks.tsx @@ -6,12 +6,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { AlertTriangle, Plus, X } from 'lucide-react'; import { Loader } from '@/components/ui/loader'; import { tasksApi } from '@/lib/api'; -import type { - GitBranch, - TaskAttempt, - RepoBranchStatus, - RepositoryBranches, -} from 'shared/types'; +import type { TaskAttempt, RepoBranchStatus } from 'shared/types'; import { openTaskForm } from '@/lib/openTaskForm'; import { FeatureShowcaseDialog } from '@/components/dialogs/global/FeatureShowcaseDialog'; import { showcases } from '@/config/showcases'; @@ -24,8 +19,6 @@ import { useTaskAttempts } from '@/hooks/useTaskAttempts'; import { useTaskAttempt } from '@/hooks/useTaskAttempt'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useBranchStatus, useAttemptExecution } from '@/hooks'; -import { useAttemptRepo } from '@/hooks/useAttemptRepo'; -import { projectsApi } from '@/lib/api'; import { paths } from '@/lib/paths'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; import { ClickedElementsProvider } from '@/contexts/ClickedElementsProvider'; @@ -108,15 +101,11 @@ function GitErrorBanner() { function DiffsPanelContainer({ attempt, selectedTask, - projectId, branchStatus, - branches, }: { attempt: TaskAttempt | null; selectedTask: TaskWithAttemptStatus | null; - projectId: string; branchStatus: RepoBranchStatus[] | null; - branches: GitBranch[]; }) { const { isAttemptRunning } = useAttemptExecution(attempt?.id); @@ -127,9 +116,7 @@ function DiffsPanelContainer({ attempt && selectedTask ? { task: selectedTask, - projectId, branchStatus: branchStatus ?? null, - branches, isAttemptRunning, selectedBranch: branchStatus?.[0]?.target_branch_name ?? null, } @@ -297,23 +284,6 @@ export function ProjectTasks() { const { data: attempt } = useTaskAttempt(effectiveAttemptId); const { data: branchStatus } = useBranchStatus(attempt?.id); - const { selectedRepoId } = useAttemptRepo(attempt?.id); - const [repoBranches, setRepoBranches] = useState([]); - - useEffect(() => { - if (!projectId) return; - projectsApi - .getBranches(projectId) - .then(setRepoBranches) - .catch(() => setRepoBranches([])); - }, [projectId]); - - const branches = useMemo( - () => - repoBranches.find((r) => r.repository_id === selectedRepoId)?.branches ?? - [], - [repoBranches, selectedRepoId] - ); const rawMode = searchParams.get('view') as LayoutMode; const mode: LayoutMode = @@ -1028,9 +998,7 @@ export function ProjectTasks() { )} diff --git a/frontend/src/pages/settings/ProjectSettings.tsx b/frontend/src/pages/settings/ProjectSettings.tsx index c32fb229..7b1bb0f1 100644 --- a/frontend/src/pages/settings/ProjectSettings.tsx +++ b/frontend/src/pages/settings/ProjectSettings.tsx @@ -30,7 +30,7 @@ import { CopyFilesField } from '@/components/projects/CopyFilesField'; import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea'; import { RepoPickerDialog } from '@/components/dialogs/shared/RepoPickerDialog'; import { projectsApi } from '@/lib/api'; -import { branchKeys } from '@/hooks/useBranches'; +import { repoBranchKeys } from '@/hooks/useRepoBranches'; import type { Project, ProjectRepo, Repo, UpdateProject } from 'shared/types'; interface ProjectFormState { @@ -330,7 +330,7 @@ export function ProjectSettings() { queryKey: ['projectRepositories', selectedProjectId], }); queryClient.invalidateQueries({ - queryKey: branchKeys.byProject(selectedProjectId), + queryKey: repoBranchKeys.byRepo(newRepo.id), }); } catch (err) { setRepoError( @@ -353,7 +353,7 @@ export function ProjectSettings() { queryKey: ['projectRepositories', selectedProjectId], }); queryClient.invalidateQueries({ - queryKey: branchKeys.byProject(selectedProjectId), + queryKey: repoBranchKeys.byRepo(repoId), }); } catch (err) { setRepoError( diff --git a/shared/types.ts b/shared/types.ts index 6fd8b17c..32b428b8 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -206,10 +206,6 @@ export type CheckAgentAvailabilityQuery = { executor: BaseCodingAgent, }; export type CurrentUserResponse = { user_id: string, }; -export type RepositoryBranches = { repository_id: string, repository_name: string, branches: Array, }; - -export type ProjectBranchesResponse = { repositories: Array, }; - export type CreateFollowUpAttempt = { prompt: string, variant: string | null, retry_process_id: string | null, force_when_dirty: boolean | null, perform_git_reset: boolean | null, }; export type ChangeTargetBranchRequest = { repo_id: string, new_target_branch: string, };